Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
Cache_p.h 29.53 KiB
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include <limits>
#include <optional>

#include <QDateTime>
#include <QString>

#if __has_include(<lmdbxx/lmdb++.h>)
#include <lmdbxx/lmdb++.h>
#else
#include <lmdb++.h>
#endif
#include <nlohmann/json.hpp>

#include <mtx/responses/notifications.hpp>
#include <mtx/responses/sync.hpp>
#include <mtxclient/crypto/types.hpp>
#include <mtxclient/http/client.hpp>

#include "CacheCryptoStructs.h"
#include "CacheStructs.h"
#include "Logging.h"

namespace mtx::responses {
struct Messages;
}

class Cache final : public QObject
{
    Q_OBJECT

public:
    Cache(const QString &userId, QObject *parent = nullptr);

    std::string displayName(const std::string &room_id, const std::string &user_id);
    QString displayName(const QString &room_id, const QString &user_id);
    QString avatarUrl(const QString &room_id, const QString &user_id);

    // presence
    mtx::events::presence::Presence presence(const std::string &user_id);

    // user cache stores user keys
    std::map<std::string, std::optional<UserKeyCache>>
    getMembersWithKeys(const std::string &room_id, bool verified_only);
    void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
    void markUserKeysOutOfDate(const std::vector<std::string> &user_ids);
    void markUserKeysOutOfDate(lmdb::txn &txn,
                               lmdb::dbi &db,
                               const std::vector<std::string> &user_ids,
                               const std::string &sync_token);
    void query_keys(const std::string &user_id,
                    std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);

    // device & user verification cache
    std::optional<UserKeyCache> userKeys(const std::string &user_id);
    VerificationStatus verificationStatus(const std::string &user_id);
    void markDeviceVerified(const std::string &user_id, const std::string &device);
    void markDeviceUnverified(const std::string &user_id, const std::string &device);
    crypto::Trust roomVerificationStatus(const std::string &room_id);

    std::vector<std::string> joinedRooms();
    std::map<std::string, RoomInfo> getCommonRooms(const std::string &user_id);

    QMap<QString, RoomInfo> roomInfo(bool withInvites = true);
    QHash<QString, RoomInfo> invites();
    std::optional<RoomInfo> invite(std::string_view roomid);
    QMap<QString, std::optional<RoomInfo>> spaces();

    //! Calculate & return the name of the room.
    QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
    //! Get room join rules
    mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
    bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb);
    //! Retrieve the topic of the room if any.
    QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
    //! Retrieve the room avatar's url if any.
    QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
    //! Retrieve the version of the room if any.
    QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
    //! Retrieve if the room is a space
    bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
    //! Retrieve if the room is tombstoned (closed or replaced by a different room)
    bool getRoomIsTombstoned(lmdb::txn &txn, lmdb::dbi &statesdb);

    //! Get a specific state event
    template<typename T>
    std::optional<mtx::events::StateEvent<T>>
    getStateEvent(const std::string &room_id, std::string_view state_key = "")
    {
        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
        return getStateEvent<T>(txn, room_id, state_key);
    }
    template<typename T>
    std::vector<mtx::events::StateEvent<T>>
    getStateEventsWithType(const std::string &room_id,
                           mtx::events::EventType type = mtx::events::state_content_to_type<T>)
    {
        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
        return getStateEventsWithType<T>(txn, room_id, type);
    }

    //! retrieve a specific event from account data
    //! pass empty room_id for global account data
    std::optional<mtx::events::collections::RoomAccountDataEvents>
    getAccountData(mtx::events::EventType type, const std::string &room_id = "");

    //! Retrieve member info from a room.
    std::vector<RoomMember>
    getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30);

    std::vector<RoomMember> getMembersFromInvite(const std::string &room_id,
                                                 std::size_t startIndex = 0,
                                                 std::size_t len        = 30);
    size_t memberCount(const std::string &room_id);

    void updateState(const std::string &room,
                     const mtx::responses::StateEvents &state,
                     bool wipe = false);
    void saveState(const mtx::responses::Sync &res);
    bool isInitialized();
    bool isDatabaseReady() { return databaseReady_ && isInitialized(); }

    std::string nextBatchToken();

    void deleteData();

    void removeInvite(lmdb::txn &txn, const std::string &room_id);
    void removeInvite(const std::string &room_id);
    void removeRoom(lmdb::txn &txn, const std::string &roomid);
    void removeRoom(const std::string &roomid);
    void setup();

    cache::CacheVersion formatVersion();
    void setCurrentFormat();
    bool runMigrations();

    std::vector<QString> roomIds();

    //! Retrieve all the user ids from a room.
    std::vector<std::string> roomMembers(const std::string &room_id);

    //! Check if the given user has power leve greater than than
    //! lowest power level of the given events.
    bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
                             const std::string &room_id,
                             const std::string &user_id);

    //! Adds a user to the read list for the given event.
    //!
    //! There should be only one user id present in a receipt list per room.
    //! The user id should be removed from any other lists.
    using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
    void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts);

    //! Retrieve all the read receipts for the given event id and room.
    //!
    //! Returns a map of user ids and the time of the read receipt in milliseconds.
    using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
    UserReceipts readReceipts(const QString &event_id, const QString &room_id);

    RoomInfo singleRoomInfo(const std::string &room_id);
    std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
    std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms);

    std::vector<RoomNameAlias> roomNamesAndAliases();

    void updateLastMessageTimestamp(const std::string &room_id, uint64_t ts);

    //! Calculates which the read status of a room.
    //! Whether all the events in the timeline have been read.
    std::string getFullyReadEventId(const std::string &room_id);
    bool calculateRoomReadStatus(const std::string &room_id);
    void calculateRoomReadStatus();

    void markSentNotification(const std::string &event_id);
    //! Removes an event from the sent notifications.
    void removeReadNotification(const std::string &event_id);
    //! Check if we have sent a desktop notification for the given event id.
    bool isNotificationSent(const std::string &event_id);

    std::optional<mtx::events::collections::TimelineEvents>
    getEvent(const std::string &room_id, std::string_view event_id);
    void storeEvent(const std::string &room_id,
                    const std::string &event_id,
                    const mtx::events::collections::TimelineEvents &event);
    void replaceEvent(const std::string &room_id,
                      const std::string &event_id,
                      const mtx::events::collections::TimelineEvents &event);
    std::vector<std::string> relatedEvents(const std::string &room_id, const std::string &event_id);

    struct TimelineRange
    {
        uint64_t first, last;
    };
    std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
    std::optional<uint64_t> getTimelineIndex(const std::string &room_id, std::string_view event_id);
    std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id);
    std::optional<std::pair<uint64_t, std::string>>
    lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
    std::optional<std::pair<uint64_t, std::string>>
    lastVisibleEvent(const std::string &room_id, std::string_view event_id);
    std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);

    std::string previousBatchToken(const std::string &room_id);
    uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
    void savePendingMessage(const std::string &room_id,
                            const mtx::events::collections::TimelineEvents &message);
    std::vector<std::string> pendingEvents(const std::string &room_id);
    std::optional<mtx::events::collections::TimelineEvents>
    firstPendingMessage(const std::string &room_id);
    void removePendingStatus(const std::string &room_id, const std::string &txn_id);

    //! clear timeline keeping only the latest batch
    void clearTimeline(const std::string &room_id);

    //! Remove old unused data.
    void deleteOldMessages();
    void deleteOldData() noexcept;
    //! Retrieve all saved room ids.
    std::vector<std::string> getRoomIds(lmdb::txn &txn);
    std::vector<std::string> getParentRoomIds(const std::string &room_id);
    std::vector<std::string> getChildRoomIds(const std::string &room_id);

    std::vector<ImagePackInfo>
    getImagePacks(const std::string &room_id, std::optional<bool> stickers);

    //! Mark a room that uses e2e encryption.
    void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
    bool isRoomEncrypted(const std::string &room_id);
    std::optional<mtx::events::state::Encryption>
    roomEncryptionSettings(const std::string &room_id);

    //! Check if a user is a member of the room.
    bool isRoomMember(const std::string &user_id, const std::string &room_id);
    std::optional<MemberInfo>
    getInviteMember(const std::string &room_id, const std::string &user_id);

    //
    // Outbound Megolm Sessions
    //
    void saveOutboundMegolmSession(const std::string &room_id,
                                   const GroupSessionData &data,
                                   mtx::crypto::OutboundGroupSessionPtr &session);
    OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
    bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
    void updateOutboundMegolmSession(const std::string &room_id,
                                     const GroupSessionData &data,
                                     mtx::crypto::OutboundGroupSessionPtr &session);
    void dropOutboundMegolmSession(const std::string &room_id);

    void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
    mtx::crypto::ExportedSessionKeys exportSessionKeys();

    //
    // Inbound Megolm Sessions
    //
    void saveInboundMegolmSession(const MegolmSessionIndex &index,
                                  mtx::crypto::InboundGroupSessionPtr session,
                                  const GroupSessionData &data);
    mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index);
    bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
    std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index);

    //
    // Olm Sessions
    //
    void saveOlmSession(const std::string &curve25519,
                        mtx::crypto::OlmSessionPtr session,
                        uint64_t timestamp);
    void saveOlmSessions(std::vector<std::pair<std::string, mtx::crypto::OlmSessionPtr>> sessions,
                         uint64_t timestamp);
    std::vector<std::string> getOlmSessions(const std::string &curve25519);
    std::optional<mtx::crypto::OlmSessionPtr>
    getOlmSession(const std::string &curve25519, const std::string &session_id);
    std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(const std::string &curve25519);

    void saveOlmAccount(const std::string &pickled);
    std::string restoreOlmAccount();

    void saveBackupVersion(const OnlineBackupVersion &data);
    void deleteBackupVersion();
    std::optional<OnlineBackupVersion> backupVersion();

    void storeSecret(const std::string &name, const std::string &secret, bool internal = false);
    void deleteSecret(const std::string &name, bool internal = false);
    std::optional<std::string> secret(const std::string &name, bool internal = false);

    std::string pickleSecret();

    template<class T>
    constexpr static bool isStateEvent_ =
      std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
                     mtx::events::StateEvent<decltype(std::declval<T>().content)>>;

    static int compare_state_key(const MDB_val *a, const MDB_val *b)
    {
        auto get_skey = [](const MDB_val *v) {
            auto temp = std::string_view(static_cast<const char *>(v->mv_data), v->mv_size);
            // allow only passing the state key, in which case no null char will be in it and we
            // return the whole string because rfind returns npos.
            // We search from the back, because state keys could include nullbytes, event ids can
            // not.
            return temp.substr(0, temp.rfind('\0'));
        };

        return get_skey(a).compare(get_skey(b));
    }

signals:
    void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
    void roomReadStatus(const std::map<QString, bool> &status);
    void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
    void userKeysUpdateFinalize(const std::string &user_id);
    void verificationStatusChanged(const std::string &userid);
    void selfVerificationStatusChanged();
    void secretChanged(const std::string name);
    void databaseReady();

private:
    void loadSecretsFromStore(
      std::vector<std::pair<std::string, bool>> toLoad,
      std::function<void(const std::string &name, bool internal, const std::string &value)>
        callback,
      bool databaseReadyOnFinished = false);
    void storeSecretInStore(const std::string name, const std::string secret);
    void deleteSecretFromStore(const std::string name, bool internal);

    //! Save an invited room.
    void saveInvite(lmdb::txn &txn,
                    lmdb::dbi &statesdb,
                    lmdb::dbi &membersdb,
                    const mtx::responses::InvitedRoom &room);

    QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
    QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
    QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
    bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);

    std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id);

    std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
    void saveTimelineMessages(lmdb::txn &txn,
                              lmdb::dbi &eventsDb,
                              const std::string &room_id,
                              const mtx::responses::Timeline &res);

    //! retrieve a specific event from account data
    //! pass empty room_id for global account data
    std::optional<mtx::events::collections::RoomAccountDataEvents>
    getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
    bool isHiddenEvent(lmdb::txn &txn,
                       mtx::events::collections::TimelineEvents e,
                       const std::string &room_id);

    //! Remove a room from the cache.
    // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
    template<class T>
    void saveStateEvents(lmdb::txn &txn,
                         lmdb::dbi &statesdb,
                         lmdb::dbi &stateskeydb,
                         lmdb::dbi &membersdb,
                         lmdb::dbi &eventsDb,
                         const std::string &room_id,
                         const std::vector<T> &events)
    {
        for (const auto &e : events)
            saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
    }

    template<class T>
    void saveStateEvent(lmdb::txn &txn,
                        lmdb::dbi &statesdb,
                        lmdb::dbi &stateskeydb,
                        lmdb::dbi &membersdb,
                        lmdb::dbi &eventsDb,
                        const std::string &room_id,
                        const T &event)
    {
        using namespace mtx::events;
        using namespace mtx::events::state;

        if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) {
            switch (e->content.membership) {
            //
            // We only keep users with invite or join membership.
            //
            case Membership::Invite:
            case Membership::Join: {
                auto display_name =
                  e->content.display_name.empty() ? e->state_key : e->content.display_name;

                std::string inviter = "";
                if (e->content.membership == mtx::events::state::Membership::Invite) {
                    inviter = e->sender;
                }

                // Lightweight representation of a member.
                MemberInfo tmp{
                  display_name,
                  e->content.avatar_url,
                  inviter,
                  e->content.reason,
                  e->content.is_direct,
                };

                membersdb.put(txn, e->state_key, nlohmann::json(tmp).dump());
                break;
            }
            default: {
                membersdb.del(txn, e->state_key, "");
                break;
            }
            }
        } else if (auto encr = std::get_if<StateEvent<Encryption>>(&event)) {
            if (!encr->state_key.empty())
                return;

            setEncryptedRoom(txn, room_id);

            std::string_view temp;
            // ensure we don't replace the event in the db
            if (statesdb.get(txn, to_string(encr->type), temp)) {
                return;
            }
        }

        std::visit(
          [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
              if constexpr (isStateEvent_<decltype(e)>) {
                  eventsDb.put(txn, e.event_id, nlohmann::json(e).dump());

                  if (e.type != EventType::Unsupported) {
                      if (std::is_same_v<std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
                                         StateEvent<mtx::events::msg::Redacted>>) {
                          // apply the redaction event
                          if (e.type == EventType::RoomMember) {
                              // membership is not revoked, but names are yeeted (so we set the name
                              // to the mxid)
                              MemberInfo tmp{e.state_key, ""};
                              membersdb.put(txn, e.state_key, nlohmann::json(tmp).dump());
                          } else if (e.state_key.empty()) {
                              // strictly speaking some stuff in those events can be redacted, but
                              // this is close enough. Ref:
                              // https://spec.matrix.org/v1.6/rooms/v10/#redactions
                              if (e.type != EventType::RoomCreate &&
                                  e.type != EventType::RoomJoinRules &&
                                  e.type != EventType::RoomPowerLevels &&
                                  e.type != EventType::RoomHistoryVisibility)
                                  statesdb.del(txn, to_string(e.type));
                          } else
                              stateskeydb.del(
                                txn, to_string(e.type), e.state_key + '\0' + e.event_id);
                      } else if (e.state_key.empty()) {
                          statesdb.put(txn, to_string(e.type), nlohmann::json(e).dump());
                      } else {
                          auto data = e.state_key + '\0' + e.event_id;
                          auto key  = to_string(e.type);

                          // Work around https://bugs.openldap.org/show_bug.cgi?id=8447
                          stateskeydb.del(txn, key, data);
                          stateskeydb.put(txn, key, data);
                      }
                  }
              }
          },
          event);
    }

    template<typename T>
    std::optional<mtx::events::StateEvent<T>>
    getStateEvent(lmdb::txn &txn, const std::string &room_id, std::string_view state_key = "")
    {
        try {
            constexpr auto type = mtx::events::state_content_to_type<T>;
            static_assert(type != mtx::events::EventType::Unsupported,
                          "Not a supported type in state events.");

            if (room_id.empty())
                return std::nullopt;
            const auto typeStr = to_string(type);

            std::string_view value;
            if (state_key.empty()) {
                auto db = getStatesDb(txn, room_id);
                if (!db.get(txn, typeStr, value)) {
                    return std::nullopt;
                }
            } else {
                auto db = getStatesKeyDb(txn, room_id);
                // we can search using state key, since the compare functions defaults to the whole
                // string, when there is no nullbyte
                std::string_view data     = state_key;
                std::string_view typeStrV = typeStr;

                auto cursor = lmdb::cursor::open(txn, db);
                if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
                    return std::nullopt;

                try {
                    auto eventsDb = getEventsDb(txn, room_id);
                    auto eventid  = data;
                    if (auto sep = data.rfind('\0'); sep != std::string_view::npos) {
                        if (!eventsDb.get(txn, eventid.substr(sep + 1), value))
                            return std::nullopt;
                    } else {
                        return std::nullopt;
                    }

                } catch (std::exception &) {
                    return std::nullopt;
                }
            }

            return nlohmann::json::parse(value).get<mtx::events::StateEvent<T>>();
        } catch (std::exception &) {
            return std::nullopt;
        }
    }

    template<typename T>
    std::vector<mtx::events::StateEvent<T>>
    getStateEventsWithType(lmdb::txn &txn,
                           const std::string &room_id,
                           mtx::events::EventType type = mtx::events::state_content_to_type<T>)

    {
        if (room_id.empty())
            return {};

        std::vector<mtx::events::StateEvent<T>> events;

        {
            auto db                   = getStatesKeyDb(txn, room_id);
            auto eventsDb             = getEventsDb(txn, room_id);
            const auto typeStr        = to_string(type);
            std::string_view typeStrV = typeStr;
            std::string_view data;
            std::string_view value;

            auto cursor = lmdb::cursor::open(txn, db);
            bool first  = true;
            if (cursor.get(typeStrV, data, MDB_SET)) {
                while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
                    first = false;

                    try {
                        auto eventid = data;
                        if (auto sep = data.rfind('\0'); sep != std::string_view::npos) {
                            if (eventsDb.get(txn, eventid.substr(sep + 1), value))
                                events.push_back(
                                  nlohmann::json::parse(value).get<mtx::events::StateEvent<T>>());
                        }
                    } catch (std::exception &e) {
                        nhlog::db()->warn("Failed to parse state event: {}", e.what());
                    }
                }
            }
        }

        return events;
    }
    void
    saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &rooms);

    void savePresence(
      lmdb::txn &txn,
      const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates);

    //! Sends signals for the rooms that are removed.
    void
    removeLeftRooms(lmdb::txn &txn, const std::map<std::string, mtx::responses::LeftRoom> &rooms)
    {
        for (const auto &room : rooms) {
            removeRoom(txn, room.first);

            // Clean up leftover invites.
            removeInvite(txn, room.first);
        }
    }

    void updateSpaces(lmdb::txn &txn,
                      const std::set<std::string> &spaces_with_updates,
                      std::set<std::string> rooms_with_updates);

    lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
    {
        return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
    }

    lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
    }

    lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(
          txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
    }

    // inverse of EventOrderDb
    lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
    }

    lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
    }

    lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(
          txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
    }

    lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(
          txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
    }

    lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(
          txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
    }

    lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
    }

    lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
    }

    lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
    }

    lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
    {
        auto db = lmdb::dbi::open(
          txn, std::string(room_id + "/states_key").c_str(), MDB_CREATE | MDB_DUPSORT);
        lmdb::dbi_set_dupsort(txn, db, compare_state_key);
        return db;
    }

    lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
    }

    lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
    {
        return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
    }

    lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); }

    lmdb::dbi getVerificationDb(lmdb::txn &txn)
    {
        return lmdb::dbi::open(txn, "verified", MDB_CREATE);
    }

    //! Retrieves or creates the database that stores the open OLM sessions between our device
    //! and the given curve25519 key which represents another device.
    //!
    //! Each entry is a map from the session_id to the pickled representation of the session.
    lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key)
    {
        return lmdb::dbi::open(
          txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE);
    }

    QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event)
    {
        if (!event.content.display_name.empty())
            return QString::fromStdString(event.content.display_name);

        return QString::fromStdString(event.state_key);
    }

    std::optional<VerificationCache> verificationCache(const std::string &user_id, lmdb::txn &txn);
    VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
    std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);

    void setNextBatchToken(lmdb::txn &txn, const std::string &token);

    lmdb::env env_;
    lmdb::dbi syncStateDb_;
    lmdb::dbi roomsDb_;
    lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
    lmdb::dbi invitesDb_;
    lmdb::dbi readReceiptsDb_;
    lmdb::dbi notificationsDb_;
    lmdb::dbi presenceDb_;

    lmdb::dbi devicesDb_;
    lmdb::dbi deviceKeysDb_;

    lmdb::dbi inboundMegolmSessionDb_;
    lmdb::dbi outboundMegolmSessionDb_;
    lmdb::dbi megolmSessionDataDb_;

    lmdb::dbi encryptedRooms_;

    QString localUserId_;
    QString cacheDirectory_;

    std::string pickle_secret_;

    VerificationStorage verification_storage;

    bool databaseReady_ = false;
};

namespace cache {
Cache *
client();
}