Skip to content
Snippets Groups Projects
Cache.cpp 89.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU General Public License as published by
     * the Free Software Foundation, either version 3 of the License, or
     * (at your option) any later version.
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU General Public License for more details.
     *
     * You should have received a copy of the GNU General Public License
     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     */
    
    
    #include <stdexcept>
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include <variant>
    
    #include <QByteArray>
    
    #include <QCoreApplication>
    
    #include <QFile>
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    #include <QHash>
    
    #include <QStandardPaths>
    
    
    #include <mtx/responses/common.hpp>
    
    #include "Cache.h"
    
    #include "Cache_p.h"
    
    #include "EventAccessors.h"
    
    #include "Logging.h"
    
    #include "Utils.h"
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! Should be changed when a breaking change occurs in the cache format.
    //! This will reset client's data.
    
    static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.05.01");
    
    static const std::string SECRET("secret");
    
    static lmdb::val NEXT_BATCH_KEY("next_batch");
    static lmdb::val OLM_ACCOUNT_KEY("olm_account");
    static lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
    
    constexpr size_t MAX_RESTORED_MESSAGES = 30'000;
    
    constexpr auto DB_SIZE = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB
    
    constexpr auto MAX_DBS = 8092UL;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! Cache databases and their format.
    //!
    //! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
    //! Format: room_id -> RoomInfo
    
    constexpr auto ROOMS_DB("rooms");
    constexpr auto INVITES_DB("invites");
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! Keeps already downloaded media for reuse.
    //! Format: matrix_url -> binary data.
    
    constexpr auto MEDIA_DB("media");
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! Information that  must be kept between sync requests.
    
    constexpr auto SYNC_STATE_DB("sync_state");
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! Read receipts per room/event.
    
    constexpr auto READ_RECEIPTS_DB("read_receipts");
    constexpr auto NOTIFICATIONS_DB("sent_notifications");
    
    //! TODO: delete pending_receipts database on old cache versions
    
    
    //! Encryption related databases.
    
    //! user_id -> list of devices
    constexpr auto DEVICES_DB("devices");
    //! device_id -> device keys
    constexpr auto DEVICE_KEYS_DB("device_keys");
    //! room_ids that have encryption enabled.
    
    constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms");
    
    //! room_id -> pickled OlmInboundGroupSession
    
    constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions");
    //! MegolmSessionIndex -> pickled OlmOutboundGroupSession
    constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
    
    using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
    using Receipts       = std::map<std::string, std::map<std::string, uint64_t>>;
    
    
    Q_DECLARE_METATYPE(SearchResult)
    
    Q_DECLARE_METATYPE(std::vector<SearchResult>)
    
    Q_DECLARE_METATYPE(RoomMember)
    Q_DECLARE_METATYPE(mtx::responses::Timeline)
    Q_DECLARE_METATYPE(RoomSearchResult)
    Q_DECLARE_METATYPE(RoomInfo)
    
    
    std::unique_ptr<Cache> instance_ = nullptr;
    
    int
    numeric_key_comparison(const MDB_val *a, const MDB_val *b)
    
            auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size));
            auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size));
    
            if (lhs < rhs)
                    return 1;
            else if (lhs == rhs)
                    return 0;
    
            return -1;
    
    Cache::Cache(const QString &userId, QObject *parent)
      : QObject{parent}
      , env_{nullptr}
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
      , syncStateDb_{0}
      , roomsDb_{0}
    
      , invitesDb_{0}
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
      , mediaDb_{0}
    
      , readReceiptsDb_{0}
    
      , devicesDb_{0}
      , deviceKeysDb_{0}
      , inboundMegolmSessionDb_{0}
      , outboundMegolmSessionDb_{0}
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
      , localUserId_{userId}
    
            nhlog::db()->debug("setting up cache");
    
            auto statePath = QString("%1/%2")
    
                               .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                               .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
    
            cacheDirectory_ = QString("%1/%2")
                                .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                                .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
    
            bool isInitial = !QFile::exists(statePath);
    
            env_.set_mapsize(DB_SIZE);
            env_.set_max_dbs(MAX_DBS);
    
                    nhlog::db()->info("initializing LMDB");
    
                    if (!QDir().mkpath(statePath)) {
                            throw std::runtime_error(
                              ("Unable to create state directory:" + statePath).toStdString().c_str());
                    }
            }
    
            try {
                    env_.open(statePath.toStdString().c_str());
            } catch (const lmdb::error &e) {
                    if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
                            throw std::runtime_error("LMDB initialization failed" +
                                                     std::string(e.what()));
                    }
    
                    nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
    
                    for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
                            if (!stateDir.remove(file))
                                    throw std::runtime_error(
                                      ("Unable to delete file " + file).toStdString().c_str());
                    }
    
                    env_.open(statePath.toStdString().c_str());
            }
    
            auto txn         = lmdb::txn::begin(env_);
            syncStateDb_     = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
            roomsDb_         = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
            invitesDb_       = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
            mediaDb_         = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
            readReceiptsDb_  = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
            notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
    
    
            // Device management
            devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
            deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
    
            // Session management
            inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
            outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
    
            txn.commit();
    }
    
    
    Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
    
            nhlog::db()->info("mark room {} as encrypted", room_id);
    
            auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
    
            lmdb::dbi_put(txn, db, lmdb::val(room_id), lmdb::val("0"));
    }
    
    bool
    Cache::isRoomEncrypted(const std::string &room_id)
    {
            lmdb::val unused;
    
            auto txn = lmdb::txn::begin(env_);
            auto db  = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
            auto res = lmdb::dbi_get(txn, db, lmdb::val(room_id), unused);
            txn.commit();
    
            return res;
    }
    
    
    mtx::crypto::ExportedSessionKeys
    Cache::exportSessionKeys()
    {
            using namespace mtx::crypto;
    
            ExportedSessionKeys keys;
    
            auto txn    = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
    
            std::string key, value;
            while (cursor.get(key, value, MDB_NEXT)) {
                    ExportedSession exported;
    
                    MegolmSessionIndex index;
    
    
                    auto saved_session = unpickle<InboundSessionObject>(value, SECRET);
    
    
                    try {
                            index = nlohmann::json::parse(key).get<MegolmSessionIndex>();
                    } catch (const nlohmann::json::exception &e) {
                            nhlog::db()->critical("failed to export megolm session: {}", e.what());
                            continue;
                    }
    
    
                    exported.room_id     = index.room_id;
                    exported.sender_key  = index.sender_key;
                    exported.session_id  = index.session_id;
                    exported.session_key = export_session(saved_session.get());
    
                    keys.sessions.push_back(exported);
            }
    
            cursor.close();
            txn.commit();
    
            return keys;
    }
    
    void
    Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
    {
            for (const auto &s : keys.sessions) {
                    MegolmSessionIndex index;
                    index.room_id    = s.room_id;
                    index.session_id = s.session_id;
                    index.sender_key = s.sender_key;
    
                    auto exported_session = mtx::crypto::import_session(s.session_key);
    
                    saveInboundMegolmSession(index, std::move(exported_session));
            }
    }
    
    
    //
    // Session Management
    //
    
    void
    Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
                                    mtx::crypto::InboundGroupSessionPtr session)
    {
            using namespace mtx::crypto;
    
            const auto key     = json(index).dump();
    
            const auto pickled = pickle<InboundSessionObject>(session.get(), SECRET);
    
            auto txn = lmdb::txn::begin(env_);
            lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled));
            txn.commit();
    
            {
                    std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
                    session_storage.group_inbound_sessions[key] = std::move(session);
            }
    }
    
    OlmInboundGroupSession *
    Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
    {
            std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
    
            return session_storage.group_inbound_sessions[json(index).dump()].get();
    
    Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index)
    
    {
            std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
    
            return session_storage.group_inbound_sessions.find(json(index).dump()) !=
    
                   session_storage.group_inbound_sessions.end();
    }
    
    
    void
    Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index)
    {
            using namespace mtx::crypto;
    
            if (!outboundMegolmSessionExists(room_id))
                    return;
    
            OutboundGroupSessionData data;
            OlmOutboundGroupSession *session;
            {
                    std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
                    data    = session_storage.group_outbound_session_data[room_id];
                    session = session_storage.group_outbound_sessions[room_id].get();
    
                    // Update with the current message.
                    data.message_index                                   = message_index;
                    session_storage.group_outbound_session_data[room_id] = data;
            }
    
            // Save the updated pickled data for the session.
            json j;
            j["data"]    = data;
            j["session"] = pickle<OutboundSessionObject>(session, SECRET);
    
            auto txn = lmdb::txn::begin(env_);
            lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
            txn.commit();
    }
    
    
    Cache::saveOutboundMegolmSession(const std::string &room_id,
    
                                     const OutboundGroupSessionData &data,
                                     mtx::crypto::OutboundGroupSessionPtr session)
    {
            using namespace mtx::crypto;
            const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
    
            json j;
            j["data"]    = data;
            j["session"] = pickled;
    
            auto txn = lmdb::txn::begin(env_);
    
            lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
    
            txn.commit();
    
            {
                    std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
    
                    session_storage.group_outbound_session_data[room_id] = data;
                    session_storage.group_outbound_sessions[room_id]     = std::move(session);
    
    Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
    
    {
            std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
    
            return (session_storage.group_outbound_sessions.find(room_id) !=
    
                    session_storage.group_outbound_sessions.end()) &&
    
                   (session_storage.group_outbound_session_data.find(room_id) !=
    
                    session_storage.group_outbound_session_data.end());
    }
    
    OutboundGroupSessionDataRef
    
    Cache::getOutboundMegolmSession(const std::string &room_id)
    
    {
            std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
    
            return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(),
                                               session_storage.group_outbound_session_data[room_id]};
    
    Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session)
    
    {
            using namespace mtx::crypto;
    
            auto txn = lmdb::txn::begin(env_);
    
            auto db  = getOlmSessionsDb(txn, curve25519);
    
            const auto pickled    = pickle<SessionObject>(session.get(), SECRET);
            const auto session_id = mtx::crypto::session_id(session.get());
    
            lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled));
    
            txn.commit();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    std::optional<mtx::crypto::OlmSessionPtr>
    
    Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
    
            using namespace mtx::crypto;
    
            auto txn = lmdb::txn::begin(env_);
            auto db  = getOlmSessionsDb(txn, curve25519);
    
            lmdb::val pickled;
            bool found = lmdb::dbi_get(txn, db, lmdb::val(session_id), pickled);
    
            txn.commit();
    
            if (found) {
                    auto data = std::string(pickled.data(), pickled.size());
                    return unpickle<SessionObject>(data, SECRET);
            }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return std::nullopt;
    
    std::vector<std::string>
    Cache::getOlmSessions(const std::string &curve25519)
    
            using namespace mtx::crypto;
    
            auto txn = lmdb::txn::begin(env_);
            auto db  = getOlmSessionsDb(txn, curve25519);
    
            std::string session_id, unused;
            std::vector<std::string> res;
    
            auto cursor = lmdb::cursor::open(txn, db);
            while (cursor.get(session_id, unused, MDB_NEXT))
                    res.emplace_back(session_id);
            cursor.close();
    
            txn.commit();
    
            return res;
    
    }
    
    void
    Cache::saveOlmAccount(const std::string &data)
    {
            auto txn = lmdb::txn::begin(env_);
            lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data));
            txn.commit();
    }
    
    void
    Cache::restoreSessions()
    {
            using namespace mtx::crypto;
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            std::string key, value;
    
            //
            // Inbound Megolm Sessions
            //
            {
                    auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
                    while (cursor.get(key, value, MDB_NEXT)) {
                            auto session = unpickle<InboundSessionObject>(value, SECRET);
                            session_storage.group_inbound_sessions[key] = std::move(session);
                    }
                    cursor.close();
            }
    
            //
            // Outbound Megolm Sessions
            //
            {
                    auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_);
                    while (cursor.get(key, value, MDB_NEXT)) {
                            json obj;
    
                            try {
                                    obj = json::parse(value);
    
                                    session_storage.group_outbound_session_data[key] =
                                      obj.at("data").get<OutboundGroupSessionData>();
    
                                    auto session =
                                      unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
                                    session_storage.group_outbound_sessions[key] = std::move(session);
                            } catch (const nlohmann::json::exception &e) {
    
                                      "failed to parse outbound megolm session data: {}", e.what());
    
            nhlog::db()->info("sessions restored");
    
    }
    
    std::string
    Cache::restoreOlmAccount()
    {
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            lmdb::val pickled;
            lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled);
    
    
            return std::string(pickled.data(), pickled.size());
    
    Cache::saveImage(const std::string &url, const std::string &img_data)
    
            if (url.empty() || img_data.empty())
                    return;
    
    
            try {
                    auto txn = lmdb::txn::begin(env_);
    
                    lmdb::dbi_put(txn,
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                                  mediaDb_,
    
                                  lmdb::val(url.data(), url.size()),
                                  lmdb::val(img_data.data(), img_data.size()));
    
    
                    txn.commit();
            } catch (const lmdb::error &e) {
    
                    nhlog::db()->critical("saveImage: {}", e.what());
    
    void
    Cache::saveImage(const QString &url, const QByteArray &image)
    {
            saveImage(url.toStdString(), std::string(image.constData(), image.length()));
    }
    
    
    QByteArray
    Cache::image(lmdb::txn &txn, const std::string &url) const
    {
            if (url.empty())
                    return QByteArray();
    
            try {
                    lmdb::val image;
                    bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(url), image);
    
                    if (!res)
                            return QByteArray();
    
                    return QByteArray(image.data(), image.size());
            } catch (const lmdb::error &e) {
    
                    nhlog::db()->critical("image: {}, {}", e.what(), url);
    
            }
    
            return QByteArray();
    }
    
    
    QByteArray
    Cache::image(const QString &url) const
    {
    
            if (url.isEmpty())
                    return QByteArray();
    
    
            auto key = url.toUtf8();
    
            try {
                    auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
                    lmdb::val image;
    
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                    bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(key.data(), key.size()), image);
    
    
                    txn.commit();
    
                    if (!res)
                            return QByteArray();
    
                    return QByteArray(image.data(), image.size());
            } catch (const lmdb::error &e) {
    
                    nhlog::db()->critical("image: {} {}", e.what(), url.toStdString());
    
    void
    Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
    {
            lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr);
            lmdb::dbi_drop(txn, getInviteStatesDb(txn, room_id), true);
            lmdb::dbi_drop(txn, getInviteMembersDb(txn, room_id), true);
    }
    
    
    void
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::removeInvite(const std::string &room_id)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            auto txn = lmdb::txn::begin(env_);
    
            removeInvite(txn, room_id);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            txn.commit();
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::removeRoom(lmdb::txn &txn, const std::string &roomid)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr);
    
            lmdb::dbi_drop(txn, getStatesDb(txn, roomid), true);
            lmdb::dbi_drop(txn, getMembersDb(txn, roomid), true);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::removeRoom(const std::string &roomid)
    
    {
            auto txn = lmdb::txn::begin(env_, nullptr, 0);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_put(txn, syncStateDb_, NEXT_BATCH_KEY, lmdb::val(token.data(), token.size()));
    
    void
    
    Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            setNextBatchToken(txn, token.toStdString());
    
    bool
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            lmdb::val token;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            bool res = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            lmdb::val token;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
    
            return std::string(token.data(), token.size());
    
    
    void
    Cache::deleteData()
    {
    
            // TODO: We need to remove the env_ while not accepting new requests.
            if (!cacheDirectory_.isEmpty()) {
    
                    QDir(cacheDirectory_).removeRecursively();
    
                    nhlog::db()->info("deleted cache files from disk");
    
    //! migrates db to the current format
    
    Cache::runMigrations()
    {
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
            lmdb::val current_version;
            bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version);
    
            txn.commit();
    
            if (!res)
                    return false;
    
            std::string stored_version(current_version.data(), current_version.size());
    
            std::vector<std::pair<std::string, std::function<bool()>>> migrations{
              {"2020.05.01",
               [this]() {
                       try {
                               auto txn = lmdb::txn::begin(env_, nullptr);
                               auto pending_receipts =
                                 lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
                               lmdb::dbi_drop(txn, pending_receipts, true);
                               txn.commit();
                       } catch (const lmdb::error &) {
                               nhlog::db()->critical(
                                 "Failed to delete pending_receipts database in migration!");
                               return false;
                       }
    
                       nhlog::db()->info("Successfully deleted pending receipts database.");
                       return true;
               }},
            };
    
            for (const auto &[target_version, migration] : migrations) {
                    if (target_version > stored_version)
                            if (!migration()) {
                                    nhlog::db()->critical("migration failure!");
                                    return false;
                            }
            }
    
            setCurrentFormat();
    
            return true;
    }
    
    cache::CacheVersion
    Cache::formatVersion()
    
    {
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
            lmdb::val current_version;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version);
    
                    return cache::CacheVersion::Older;
    
    
            std::string stored_version(current_version.data(), current_version.size());
    
    
            if (stored_version < CURRENT_CACHE_FORMAT_VERSION)
                    return cache::CacheVersion::Older;
            else if (stored_version > CURRENT_CACHE_FORMAT_VERSION)
                    return cache::CacheVersion::Older;
            else
                    return cache::CacheVersion::Current;
    
    }
    
    void
    Cache::setCurrentFormat()
    {
            auto txn = lmdb::txn::begin(env_);
    
            lmdb::dbi_put(
              txn,
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
              syncStateDb_,
    
              CACHE_FORMAT_VERSION_KEY,
              lmdb::val(CURRENT_CACHE_FORMAT_VERSION.data(), CURRENT_CACHE_FORMAT_VERSION.size()));
    
            txn.commit();
    }
    
    Cache::readReceipts(const QString &event_id, const QString &room_id)
    {
    
    
            ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
            nlohmann::json json_key = receipt_key;
    
            try {
                    auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
                    auto key = json_key.dump();
    
                    lmdb::val value;
    
                    bool res =
                      lmdb::dbi_get(txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), value);
    
                    txn.commit();
    
                    if (res) {
                            auto json_response = json::parse(std::string(value.data(), value.size()));
    
                            auto values        = json_response.get<std::map<std::string, uint64_t>>();
    
                            for (const auto &v : values)
    
                                    // timestamp, user_id
                                    receipts.emplace(v.second, v.first);
    
                    }
    
            } catch (const lmdb::error &e) {
    
                    nhlog::db()->critical("readReceipts: {}", e.what());
    
    Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
    
            auto user_id = this->localUserId_.toStdString();
    
            for (const auto &receipt : receipts) {
    
                    const auto event_id = receipt.first;
                    auto event_receipts = receipt.second;
    
                    ReadReceiptKey receipt_key{event_id, room_id};
                    nlohmann::json json_key = receipt_key;
    
                    try {
                            const auto key = json_key.dump();
    
                            lmdb::val prev_value;
    
                            bool exists = lmdb::dbi_get(
    
                              txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), prev_value);
    
                            std::map<std::string, uint64_t> saved_receipts;
    
    
                            // If an entry for the event id already exists, we would
                            // merge the existing receipts with the new ones.
                            if (exists) {
                                    auto json_value =
                                      json::parse(std::string(prev_value.data(), prev_value.size()));
    
                                    // Retrieve the saved receipts.
    
                                    saved_receipts = json_value.get<std::map<std::string, uint64_t>>();
    
                            }
    
                            // Append the new ones.
    
                            for (const auto &[read_by, timestamp] : event_receipts) {
                                    if (read_by == user_id) {
                                            emit removeNotification(QString::fromStdString(room_id),
                                                                    QString::fromStdString(event_id));
                                    }
                                    saved_receipts.emplace(read_by, timestamp);
                            }
    
    
                            // Save back the merged (or only the new) receipts.
                            nlohmann::json json_updated_value = saved_receipts;
                            std::string merged_receipts       = json_updated_value.dump();
    
                            lmdb::dbi_put(txn,
                                          readReceiptsDb_,
                                          lmdb::val(key.data(), key.size()),
                                          lmdb::val(merged_receipts.data(), merged_receipts.size()));
    
                    } catch (const lmdb::error &e) {
    
                            nhlog::db()->critical("updateReadReceipts: {}", e.what());
    
    void
    Cache::calculateRoomReadStatus()
    {
            const auto joined_rooms = joinedRooms();
    
            std::map<QString, bool> readStatus;
    
            for (const auto &room : joined_rooms)
                    readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room));
    
            emit roomReadStatus(readStatus);
    }
    
    bool
    Cache::calculateRoomReadStatus(const std::string &room_id)
    {
            auto txn = lmdb::txn::begin(env_);
    
            // Get last event id on the room.
    
            const auto last_event_id = getLastEventId(txn, room_id);
    
            const auto localUser     = utils::localUser().toStdString();
    
            txn.commit();
    
    
            if (last_event_id.empty())
                    return false;
    
    
            // Retrieve all read receipts for that event.
    
            const auto receipts =
              readReceipts(QString::fromStdString(last_event_id), QString::fromStdString(room_id));
    
    
            if (receipts.size() == 0)
                    return true;
    
            // Check if the local user has a read receipt for it.
            for (auto it = receipts.cbegin(); it != receipts.cend(); it++) {
                    if (it->second == localUser)
                            return false;
            }
    
            return true;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    void
    Cache::saveState(const mtx::responses::Sync &res)
    {
    
            using namespace mtx::events;
    
            auto user_id = this->localUserId_.toStdString();
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            auto txn = lmdb::txn::begin(env_);
    
            setNextBatchToken(txn, res.next_batch);
    
            // Save joined rooms
            for (const auto &room : res.rooms.join) {
                    auto statesdb  = getStatesDb(txn, room.first);
                    auto membersdb = getMembersDb(txn, room.first);
    
                    saveStateEvents(txn, statesdb, membersdb, room.first, room.second.state.events);
                    saveStateEvents(txn, statesdb, membersdb, room.first, room.second.timeline.events);
    
    
                    saveTimelineMessages(txn, room.first, room.second.timeline);
    
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                    RoomInfo updatedInfo;
                    updatedInfo.name  = getRoomName(txn, statesdb, membersdb).toStdString();
                    updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString();
                    updatedInfo.avatar_url =
                      getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first))
                        .toStdString();
    
                    updatedInfo.version = getRoomVersion(txn, statesdb).toStdString();
    
                    // Process the account_data associated with this room
                    bool has_new_tags = false;
                    for (const auto &evt : room.second.account_data.events) {
                            // for now only fetch tag events
    
                            if (std::holds_alternative<Event<account_data::Tags>>(evt)) {
                                    auto tags_evt = std::get<Event<account_data::Tags>>(evt);
    
                                    has_new_tags  = true;
                                    for (const auto &tag : tags_evt.content.tags) {
                                            updatedInfo.tags.push_back(tag.first);
                                    }
                            }
                    }
                    if (!has_new_tags) {
                            // retrieve the old tags, they haven't changed
                            lmdb::val data;
                            if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room.first), data)) {
                                    try {
                                            RoomInfo tmp =
                                              json::parse(std::string(data.data(), data.size()));
                                            updatedInfo.tags = tmp.tags;
                                    } catch (const json::exception &e) {
                                            nhlog::db()->warn(
                                              "failed to parse room info: room_id ({}), {}",
                                              room.first,
                                              std::string(data.data(), data.size()));
                                    }
                            }
                    }
    
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                    lmdb::dbi_put(
                      txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump()));
    
    
                    updateReadReceipt(txn, room.first, room.second.ephemeral.receipts);
    
    
                    // Clean up non-valid invites.
                    removeInvite(txn, room.first);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            }
    
            saveInvites(txn, res.rooms.invite);
    
    
            savePresence(txn, res.presence);
    
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            removeLeftRooms(txn, res.rooms.leave);
    
            txn.commit();
    
            std::map<QString, bool> readStatus;
    
    
            for (const auto &room : res.rooms.join) {
    
                    if (!room.second.ephemeral.receipts.empty()) {
                            std::vector<QString> receipts;
    
                            for (const auto &receipt : room.second.ephemeral.receipts) {
                                    for (const auto &receiptUsersTs : receipt.second) {
                                            if (receiptUsersTs.first != user_id) {
                                                    receipts.push_back(
                                                      QString::fromStdString(receipt.first));
                                                    break;
                                            }
                                    }
                            }
    
                            if (!receipts.empty())
                                    emit newReadReceipts(QString::fromStdString(room.first), receipts);
    
                    readStatus.emplace(QString::fromStdString(room.first),
                                       calculateRoomReadStatus(room.first));
    
    
            emit roomReadStatus(readStatus);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    }
    
    void
    Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &rooms)
    {
            for (const auto &room : rooms) {
                    auto statesdb  = getInviteStatesDb(txn, room.first);
                    auto membersdb = getInviteMembersDb(txn, room.first);
    
                    saveInvite(txn, statesdb, membersdb, room.second);
    
                    RoomInfo updatedInfo;
                    updatedInfo.name  = getInviteRoomName(txn, statesdb, membersdb).toStdString();
                    updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString();
                    updatedInfo.avatar_url =
                      getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
                    updatedInfo.is_invite = true;