Skip to content
Snippets Groups Projects
Cache.cpp 173 KiB
Newer Older
  • Learn to ignore specific revisions
  •           "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
        }
        return std::nullopt;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    std::vector<RoomMember>
    Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
    {
    
        auto txn    = ro_txn(env_);
        auto db     = getMembersDb(txn, room_id);
        auto cursor = lmdb::cursor::open(txn, db);
    
        std::size_t currentIndex = 0;
    
        const auto endIndex = std::min(startIndex + len, db.size(txn));
    
        std::vector<RoomMember> members;
    
        std::string_view user_id, user_data;
        while (cursor.get(user_id, user_data, MDB_NEXT)) {
            if (currentIndex < startIndex) {
                currentIndex += 1;
                continue;
            }
    
            if (currentIndex >= endIndex)
                break;
    
                MemberInfo tmp = nlohmann::json::parse(user_data).get<MemberInfo>();
    
                members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)),
                                                QString::fromStdString(tmp.name)});
    
            } catch (const nlohmann::json::exception &e) {
    
                nhlog::db()->warn("{}", e.what());
    
            currentIndex += 1;
        }
    
        cursor.close();
    
        return members;
    
    std::vector<RoomMember>
    
    Cache::getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len)
    
        auto txn = ro_txn(env_);
        std::vector<RoomMember> members;
    
        try {
            auto db     = getInviteMembersDb(txn, room_id);
            auto cursor = lmdb::cursor::open(txn, db);
    
            std::size_t currentIndex = 0;
    
            const auto endIndex = std::min(startIndex + len, db.size(txn));
    
            std::string_view user_id, user_data;
            while (cursor.get(user_id, user_data, MDB_NEXT)) {
                if (currentIndex < startIndex) {
                    currentIndex += 1;
                    continue;
                }
    
                if (currentIndex >= endIndex)
                    break;
    
                    MemberInfo tmp = nlohmann::json::parse(user_data).get<MemberInfo>();
    
                    members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)),
                                                    QString::fromStdString(tmp.name),
                                                    tmp.is_direct});
    
                } catch (const nlohmann::json::exception &e) {
    
                    nhlog::db()->warn("{}", e.what());
                }
    
                currentIndex += 1;
    
            cursor.close();
        } catch (const lmdb::error &e) {
            nhlog::db()->warn("Failed to retrieve members {}", e.what());
    
        return members;
    
    bool
    Cache::isRoomMember(const std::string &user_id, const std::string &room_id)
    {
    
        try {
            auto txn = ro_txn(env_);
            auto db  = getMembersDb(txn, room_id);
    
            std::string_view value;
            bool res = db.get(txn, user_id, value);
    
            return res;
        } catch (std::exception &e) {
            nhlog::db()->warn(
              "Failed to read member membership ({}) in room ({}): {}", user_id, room_id, e.what());
        }
        return false;
    
    void
    Cache::savePendingMessage(const std::string &room_id,
                              const mtx::events::collections::TimelineEvent &message)
    {
    
        auto txn      = lmdb::txn::begin(env_);
        auto eventsDb = getEventsDb(txn, room_id);
    
        mtx::responses::Timeline timeline;
        timeline.events.push_back(message.data);
        saveTimelineMessages(txn, eventsDb, room_id, timeline);
    
        auto pending = getPendingMessagesDb(txn, room_id);
    
        int64_t now = QDateTime::currentMSecsSinceEpoch();
        pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data));
    
        txn.commit();
    
    std::vector<std::string>
    Cache::pendingEvents(const std::string &room_id)
    {
        auto txn     = ro_txn(env_);
        auto pending = getPendingMessagesDb(txn, room_id);
    
        std::vector<std::string> related_ids;
    
        try {
            {
                auto pendingCursor = lmdb::cursor::open(txn, pending);
                std::string_view tsIgnored, pendingTxn;
                while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
                    related_ids.emplace_back(pendingTxn.data(), pendingTxn.size());
                }
            }
        } catch (const lmdb::error &e) {
            nhlog::db()->error("pending events error: {}", e.what());
        }
    
        return related_ids;
    }
    
    
    std::optional<mtx::events::collections::TimelineEvent>
    Cache::firstPendingMessage(const std::string &room_id)
    {
    
        auto txn     = lmdb::txn::begin(env_);
        auto pending = getPendingMessagesDb(txn, room_id);
    
        {
            auto pendingCursor = lmdb::cursor::open(txn, pending);
            std::string_view tsIgnored, pendingTxn;
            while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
                auto eventsDb = getEventsDb(txn, room_id);
                std::string_view event;
                if (!eventsDb.get(txn, pendingTxn, event)) {
                    pending.del(txn, tsIgnored, pendingTxn);
                    continue;
                }
    
                try {
                    mtx::events::collections::TimelineEvent te;
    
                    mtx::events::collections::from_json(nlohmann::json::parse(event), te);
    
                    pendingCursor.close();
                    txn.commit();
                    return te;
                } catch (std::exception &e) {
                    nhlog::db()->error("Failed to parse message from cache {}", e.what());
                    pending.del(txn, tsIgnored, pendingTxn);
                    continue;
                }
    
        txn.commit();
    
        return std::nullopt;
    
    }
    
    void
    Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id)
    {
    
        auto txn     = lmdb::txn::begin(env_);
        auto pending = getPendingMessagesDb(txn, room_id);
    
        {
            auto pendingCursor = lmdb::cursor::open(txn, pending);
            std::string_view tsIgnored, pendingTxn;
            while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
                if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
                    lmdb::cursor_del(pendingCursor);
    
        txn.commit();
    
    void
    Cache::saveTimelineMessages(lmdb::txn &txn,
    
                                lmdb::dbi &eventsDb,
    
                                const std::string &room_id,
                                const mtx::responses::Timeline &res)
    {
    
        if (res.events.empty())
            return;
    
        auto relationsDb = getRelationsDb(txn, room_id);
    
        auto orderDb     = getEventOrderDb(txn, room_id);
        auto evToOrderDb = getEventToOrderDb(txn, room_id);
        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
        auto order2msgDb = getOrderToMessageDb(txn, room_id);
        auto pending     = getPendingMessagesDb(txn, room_id);
    
        if (res.limited) {
            lmdb::dbi_drop(txn, orderDb, false);
            lmdb::dbi_drop(txn, evToOrderDb, false);
            lmdb::dbi_drop(txn, msg2orderDb, false);
            lmdb::dbi_drop(txn, order2msgDb, false);
            lmdb::dbi_drop(txn, pending, true);
        }
    
        using namespace mtx::events;
        using namespace mtx::events::state;
    
        std::string_view indexVal, val;
        uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
        auto cursor    = lmdb::cursor::open(txn, orderDb);
        if (cursor.get(indexVal, val, MDB_LAST)) {
            index = lmdb::from_sv<uint64_t>(indexVal);
        }
    
        uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
        auto msgCursor    = lmdb::cursor::open(txn, order2msgDb);
        if (msgCursor.get(indexVal, val, MDB_LAST)) {
            msgIndex = lmdb::from_sv<uint64_t>(indexVal);
        }
    
        bool first = true;
        for (const auto &e : res.events) {
            auto event  = mtx::accessors::serialize_event(e);
            auto txn_id = mtx::accessors::transaction_id(e);
    
            std::string event_id_val = event.value("event_id", "");
            if (event_id_val.empty()) {
                nhlog::db()->error("Event without id!");
                continue;
            }
    
            std::string_view event_id = event_id_val;
    
    
            nlohmann::json orderEntry = nlohmann::json::object();
            orderEntry["event_id"]    = event_id_val;
    
            if (first && !res.prev_batch.empty())
                orderEntry["prev_batch"] = res.prev_batch;
    
            std::string_view txn_order;
            if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) {
                eventsDb.put(txn, event_id, event.dump());
                eventsDb.del(txn, txn_id);
    
                std::string_view msg_txn_order;
                if (msg2orderDb.get(txn, txn_id, msg_txn_order)) {
                    order2msgDb.put(txn, msg_txn_order, event_id);
                    msg2orderDb.put(txn, event_id, msg_txn_order);
                    msg2orderDb.del(txn, txn_id);
                }
    
                orderDb.put(txn, txn_order, orderEntry.dump());
                evToOrderDb.put(txn, event_id, txn_order);
                evToOrderDb.del(txn, txn_id);
    
                auto relations = mtx::accessors::relations(e);
                if (!relations.relations.empty()) {
                    for (const auto &r : relations.relations) {
                        if (!r.event_id.empty()) {
                            relationsDb.del(txn, r.event_id, txn_id);
                            relationsDb.put(txn, r.event_id, event_id);
                        }
    
                }
    
                auto pendingCursor = lmdb::cursor::open(txn, pending);
                std::string_view tsIgnored, pendingTxn;
                while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
                    if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
                        lmdb::cursor_del(pendingCursor);
                }
            } else if (auto redaction =
                         std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
                if (redaction->redacts.empty())
                    continue;
    
                std::string_view oldEvent;
                bool success = eventsDb.get(txn, redaction->redacts, oldEvent);
                if (!success)
                    continue;
    
                mtx::events::collections::TimelineEvent te;
                try {
                    mtx::events::collections::from_json(
    
                      nlohmann::json::parse(std::string_view(oldEvent.data(), oldEvent.size())), te);
    
                    // overwrite the content and add redation data
                    std::visit(
                      [redaction](auto &ev) {
                          ev.unsigned_data.redacted_because = *redaction;
                          ev.unsigned_data.redacted_by      = redaction->event_id;
                      },
                      te.data);
                    event = mtx::accessors::serialize_event(te.data);
                    event["content"].clear();
    
                } catch (std::exception &e) {
                    nhlog::db()->error("Failed to parse message from cache {}", e.what());
                    continue;
                }
    
                eventsDb.put(txn, redaction->redacts, event.dump());
    
                eventsDb.put(txn, redaction->event_id, nlohmann::json(*redaction).dump());
    
            } else {
                first = false;
    
                // This check protects against duplicates in the timeline. If the event_id
                // is already in the DB, we skip putting it (again) in ordered DBs, and only
                // update the event itself and its relations.
                std::string_view unused_read;
                if (!evToOrderDb.get(txn, event_id, unused_read)) {
                    ++index;
    
                    nhlog::db()->debug("saving '{}'", orderEntry.dump());
    
                    cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND);
                    evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
    
                    // TODO(Nico): Allow blacklisting more event types in UI
                    if (!isHiddenEvent(txn, e, room_id)) {
                        ++msgIndex;
                        msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND);
    
                        msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
                    }
                } else {
                    nhlog::db()->warn("duplicate event '{}'", orderEntry.dump());
                }
                eventsDb.put(txn, event_id, event.dump());
    
                auto relations = mtx::accessors::relations(e);
                if (!relations.relations.empty()) {
                    for (const auto &r : relations.relations) {
                        if (!r.event_id.empty()) {
                            relationsDb.put(txn, r.event_id, event_id);
                        }
    
    uint64_t
    Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res)
    {
    
        auto txn         = lmdb::txn::begin(env_);
        auto eventsDb    = getEventsDb(txn, room_id);
        auto relationsDb = getRelationsDb(txn, room_id);
    
        auto orderDb     = getEventOrderDb(txn, room_id);
        auto evToOrderDb = getEventToOrderDb(txn, room_id);
        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
        auto order2msgDb = getOrderToMessageDb(txn, room_id);
    
        std::string_view indexVal, val;
        uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
        {
            auto cursor = lmdb::cursor::open(txn, orderDb);
            if (cursor.get(indexVal, val, MDB_FIRST)) {
                index = lmdb::from_sv<uint64_t>(indexVal);
    
        uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
        {
            auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
            if (msgCursor.get(indexVal, val, MDB_FIRST)) {
                msgIndex = lmdb::from_sv<uint64_t>(indexVal);
            }
        }
    
        if (res.chunk.empty()) {
            if (orderDb.get(txn, lmdb::to_sv(index), val)) {
    
                auto orderEntry          = nlohmann::json::parse(val);
    
                orderEntry["prev_batch"] = res.end;
                orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
                txn.commit();
            }
            return index;
        }
    
        std::string event_id_val;
        for (const auto &e : res.chunk) {
            if (std::holds_alternative<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e))
                continue;
    
            auto event                = mtx::accessors::serialize_event(e);
            event_id_val              = event["event_id"].get<std::string>();
            std::string_view event_id = event_id_val;
    
            // This check protects against duplicates in the timeline. If the event_id is
            // already in the DB, we skip putting it (again) in ordered DBs, and only update the
            // event itself and its relations.
            std::string_view unused_read;
            if (!evToOrderDb.get(txn, event_id, unused_read)) {
                --index;
    
    
                nlohmann::json orderEntry = nlohmann::json::object();
                orderEntry["event_id"]    = event_id_val;
    
    
                orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
                evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
    
                // TODO(Nico): Allow blacklisting more event types in UI
                if (!isHiddenEvent(txn, e, room_id)) {
                    --msgIndex;
                    order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id);
    
                    msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
                }
            }
            eventsDb.put(txn, event_id, event.dump());
    
            auto relations = mtx::accessors::relations(e);
            if (!relations.relations.empty()) {
                for (const auto &r : relations.relations) {
                    if (!r.event_id.empty()) {
                        relationsDb.put(txn, r.event_id, event_id);
    
        nlohmann::json orderEntry = nlohmann::json::object();
        orderEntry["event_id"]    = event_id_val;
        orderEntry["prev_batch"]  = res.end;
    
        orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
    
        txn.commit();
    
        return msgIndex;
    
    void
    Cache::clearTimeline(const std::string &room_id)
    {
    
        auto txn         = lmdb::txn::begin(env_);
        auto eventsDb    = getEventsDb(txn, room_id);
        auto relationsDb = getRelationsDb(txn, room_id);
    
        auto orderDb     = getEventOrderDb(txn, room_id);
        auto evToOrderDb = getEventToOrderDb(txn, room_id);
        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
        auto order2msgDb = getOrderToMessageDb(txn, room_id);
    
        std::string_view indexVal, val;
        auto cursor = lmdb::cursor::open(txn, orderDb);
    
        bool start                   = true;
        bool passed_pagination_token = false;
        while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
            start = false;
    
            nlohmann::json obj;
    
                obj = nlohmann::json::parse(std::string_view(val.data(), val.size()));
    
            } catch (std::exception &) {
                // workaround bug in the initial db format, where we sometimes didn't store
                // json...
                obj = {{"event_id", std::string(val.data(), val.size())}};
            }
    
            if (passed_pagination_token) {
                if (obj.count("event_id") != 0) {
                    std::string event_id = obj["event_id"].get<std::string>();
    
                    if (!event_id.empty()) {
                        evToOrderDb.del(txn, event_id);
                        eventsDb.del(txn, event_id);
                        relationsDb.del(txn, event_id);
    
                        std::string_view order{};
                        bool exists = msg2orderDb.get(txn, event_id, order);
                        if (exists) {
                            order2msgDb.del(txn, order);
                            msg2orderDb.del(txn, event_id);
                        }
    
                }
                lmdb::cursor_del(cursor);
            } else {
                if (obj.count("prev_batch") != 0)
                    passed_pagination_token = true;
    
        auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
        start          = true;
        while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
            start = false;
    
            std::string_view eventId;
            bool innerStart = true;
            bool found      = false;
            while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) {
                innerStart = false;
    
    
                nlohmann::json obj;
    
                    obj = nlohmann::json::parse(std::string_view(eventId.data(), eventId.size()));
    
                } catch (std::exception &) {
                    obj = {{"event_id", std::string(eventId.data(), eventId.size())}};
                }
    
                if (obj["event_id"] == std::string(val.data(), val.size())) {
                    found = true;
                    break;
                }
    
            if (!found)
                break;
        }
    
        if (!start) {
            do {
                lmdb::cursor_del(msgCursor);
            } while (msgCursor.get(indexVal, val, MDB_PREV));
        }
    
    
        cursor.close();
        msgCursor.close();
        txn.commit();
    
    Joe Donofry's avatar
    Joe Donofry committed
    mtx::responses::Notifications
    
    Joe Donofry's avatar
    Joe Donofry committed
    Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id)
    
    Joe Donofry's avatar
    Joe Donofry committed
    {
    
        auto db = getMentionsDb(txn, room_id);
    
        if (db.size(txn) == 0) {
            return mtx::responses::Notifications{};
        }
    
        mtx::responses::Notifications notif;
        std::string_view event_id, msg;
    
        auto cursor = lmdb::cursor::open(txn, db);
    
        while (cursor.get(event_id, msg, MDB_NEXT)) {
    
            auto obj = nlohmann::json::parse(msg);
    
            if (obj.count("event") == 0)
                continue;
    
            mtx::responses::Notification notification;
            mtx::responses::from_json(obj, notification);
    
            notif.notifications.push_back(notification);
        }
        cursor.close();
    
        std::reverse(notif.notifications.begin(), notif.notifications.end());
    
        return notif;
    
    Joe Donofry's avatar
    Joe Donofry committed
    }
    
    Joe Donofry's avatar
    Joe Donofry committed
    
    //! Add all notifications containing a user mention to the db.
    void
    Cache::saveTimelineMentions(const mtx::responses::Notifications &res)
    {
    
        QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom;
    
        // Sort into room-specific 'buckets'
        for (const auto &notif : res.notifications) {
    
            nlohmann::json val = notif;
    
            notifsByRoom[notif.room_id].push_back(notif);
        }
    
        auto txn = lmdb::txn::begin(env_);
        // Insert the entire set of mentions for each room at a time.
        QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it =
          notifsByRoom.constBegin();
        auto end = notifsByRoom.constEnd();
        while (it != end) {
            nhlog::db()->debug("Storing notifications for " + it.key());
            saveTimelineMentions(txn, it.key(), std::move(it.value()));
            ++it;
        }
    
        txn.commit();
    
    Joe Donofry's avatar
    Joe Donofry committed
    void
    Cache::saveTimelineMentions(lmdb::txn &txn,
                                const std::string &room_id,
    
    Joe Donofry's avatar
    Joe Donofry committed
                                const QList<mtx::responses::Notification> &res)
    
    Joe Donofry's avatar
    Joe Donofry committed
    {
    
        auto db = getMentionsDb(txn, room_id);
    
        using namespace mtx::events;
        using namespace mtx::events::state;
    
        for (const auto &notif : res) {
            const auto event_id = mtx::accessors::event_id(notif.event);
    
            // double check that we have the correct room_id...
            if (room_id.compare(notif.room_id) != 0) {
                return;
            }
    
            nlohmann::json obj = notif;
    
            db.put(txn, event_id, obj.dump());
        }
    
    Joe Donofry's avatar
    Joe Donofry committed
    }
    
    void
    Cache::markSentNotification(const std::string &event_id)
    {
    
        auto txn = lmdb::txn::begin(env_);
        notificationsDb_.put(txn, event_id, "");
        txn.commit();
    
    }
    
    void
    Cache::removeReadNotification(const std::string &event_id)
    {
    
        auto txn = lmdb::txn::begin(env_);
    
        notificationsDb_.del(txn, event_id);
    
        txn.commit();
    
    }
    
    bool
    Cache::isNotificationSent(const std::string &event_id)
    {
    
        auto txn = ro_txn(env_);
    
        std::string_view value;
        bool res = notificationsDb_.get(txn, event_id, value);
    
        return res;
    
    std::vector<std::string>
    Cache::getRoomIds(lmdb::txn &txn)
    {
    
        auto cursor = lmdb::cursor::open(txn, roomsDb_);
    
        std::vector<std::string> rooms;
    
        std::string_view room_id, _unused;
        while (cursor.get(room_id, _unused, MDB_NEXT))
            rooms.emplace_back(room_id);
    
        cursor.close();
    
        return rooms;
    
        std::string_view indexVal, val;
    
        auto txn      = lmdb::txn::begin(env_);
        auto room_ids = getRoomIds(txn);
    
        for (const auto &room_id : room_ids) {
            auto orderDb     = getEventOrderDb(txn, room_id);
            auto evToOrderDb = getEventToOrderDb(txn, room_id);
            auto o2m         = getOrderToMessageDb(txn, room_id);
            auto m2o         = getMessageToOrderDb(txn, room_id);
            auto eventsDb    = getEventsDb(txn, room_id);
            auto relationsDb = getRelationsDb(txn, room_id);
            auto cursor      = lmdb::cursor::open(txn, orderDb);
    
            uint64_t first, last;
            if (cursor.get(indexVal, val, MDB_LAST)) {
                last = lmdb::from_sv<uint64_t>(indexVal);
            } else {
                continue;
            }
            if (cursor.get(indexVal, val, MDB_FIRST)) {
                first = lmdb::from_sv<uint64_t>(indexVal);
            } else {
                continue;
            }
    
            size_t message_count = static_cast<size_t>(last - first);
            if (message_count < MAX_RESTORED_MESSAGES)
                continue;
    
            bool start = true;
            while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) &&
                   message_count-- > MAX_RESTORED_MESSAGES) {
                start    = false;
    
                auto obj = nlohmann::json::parse(std::string_view(val.data(), val.size()));
    
    
                if (obj.count("event_id") != 0) {
                    std::string event_id = obj["event_id"].get<std::string>();
                    evToOrderDb.del(txn, event_id);
                    eventsDb.del(txn, event_id);
    
                    relationsDb.del(txn, event_id);
    
                    std::string_view order{};
                    bool exists = m2o.get(txn, event_id, order);
                    if (exists) {
                        o2m.del(txn, order);
                        m2o.del(txn, event_id);
    
                }
                cursor.del();
    
            cursor.close();
        }
        txn.commit();
    
        try {
            deleteOldMessages();
        } catch (const lmdb::error &e) {
            nhlog::db()->error("failed to delete old messages: {}", e.what());
        }
    
    void
    Cache::updateSpaces(lmdb::txn &txn,
                        const std::set<std::string> &spaces_with_updates,
                        std::set<std::string> rooms_with_updates)
    {
    
        if (spaces_with_updates.empty() && rooms_with_updates.empty())
            return;
    
        for (const auto &space : spaces_with_updates) {
            // delete old entries
            {
                auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
                bool first          = true;
                std::string_view sp = space, space_child = "";
    
                if (cursor.get(sp, space_child, MDB_SET)) {
                    while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
                        first = false;
                        spacesParentsDb_.del(txn, space_child, space);
    
                }
                cursor.close();
                spacesChildrenDb_.del(txn, space);
            }
    
            for (const auto &event :
                 getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) {
                if (event.content.via.has_value() && event.state_key.size() > 3 &&
                    event.state_key.at(0) == '!') {
                    spacesChildrenDb_.put(txn, space, event.state_key);
                    spacesParentsDb_.put(txn, event.state_key, space);
                }
    
    
            for (const auto &r : getRoomIds(txn)) {
                if (auto parent = getStateEvent<mtx::events::state::space::Parent>(txn, r, space)) {
                    rooms_with_updates.insert(r);
                }
            }
    
        const auto space_event_type = to_string(mtx::events::EventType::SpaceChild);
    
        for (const auto &room : rooms_with_updates) {
            for (const auto &event :
                 getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) {
                if (event.content.via.has_value() && event.state_key.size() > 3 &&
                    event.state_key.at(0) == '!') {
                    const std::string &space = event.state_key;
    
                    auto pls = getStateEvent<mtx::events::state::PowerLevels>(txn, space);
    
                    if (!pls)
                        continue;
    
                    if (pls->content.user_level(event.sender) >=
                        pls->content.state_level(space_event_type)) {
                        spacesChildrenDb_.put(txn, space, room);
                        spacesParentsDb_.put(txn, room, space);
    
                    } else {
                        nhlog::db()->debug("Skipping {} in {} because of missing PL. {}: {} < {}",
                                           room,
                                           space,
                                           event.sender,
                                           pls->content.user_level(event.sender),
                                           pls->content.state_level(space_event_type));
    
    }
    
    QMap<QString, std::optional<RoomInfo>>
    Cache::spaces()
    {
    
        auto txn = ro_txn(env_);
    
        QMap<QString, std::optional<RoomInfo>> ret;
        {
            auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
            bool first  = true;
            std::string_view space_id, space_child;
            while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
                first = false;
    
                if (!space_child.empty()) {
                    std::string_view room_data;
                    if (roomsDb_.get(txn, space_id, room_data)) {
    
                        RoomInfo tmp = nlohmann::json::parse(std::move(room_data)).get<RoomInfo>();
    
                        ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), tmp);
                    } else {
                        ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), std::nullopt);
    
            cursor.close();
        }
    
        return ret;
    
    }
    
    std::vector<std::string>
    Cache::getParentRoomIds(const std::string &room_id)
    {
    
        auto txn = ro_txn(env_);
    
        std::vector<std::string> roomids;
        {
            auto cursor         = lmdb::cursor::open(txn, spacesParentsDb_);
            bool first          = true;
            std::string_view sp = room_id, space_parent;
            if (cursor.get(sp, space_parent, MDB_SET)) {
                while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
                    first = false;
    
                    if (!space_parent.empty())
                        roomids.emplace_back(space_parent);
                }
    
            cursor.close();
        }
    
        return roomids;
    
    }
    
    std::vector<std::string>
    Cache::getChildRoomIds(const std::string &room_id)
    {
    
        auto txn = ro_txn(env_);
    
        std::vector<std::string> roomids;
        {
            auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
            bool first          = true;
            std::string_view sp = room_id, space_child;
            if (cursor.get(sp, space_child, MDB_SET)) {
                while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
                    first = false;
    
                    if (!space_child.empty())
                        roomids.emplace_back(space_child);
                }
    
            cursor.close();
        }
    
        return roomids;
    
    std::vector<ImagePackInfo>
    
    Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
    
        auto txn = ro_txn(env_);
        std::vector<ImagePackInfo> infos;
    
        auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
                                          const std::string &source_room,
                                          const std::string &state_key) {
    
            bool pack_is_sticker = pack.pack ? pack.pack->is_sticker() : true;
            bool pack_is_emoji   = pack.pack ? pack.pack->is_emoji() : true;
            bool pack_matches =
              !stickers.has_value() || (stickers.value() ? pack_is_sticker : pack_is_emoji);
    
    
            ImagePackInfo info;
            info.source_room = source_room;
            info.state_key   = state_key;
            info.pack.pack   = pack.pack;
    
            for (const auto &img : pack.images) {
                if (stickers.has_value() &&
                    (img.second.overrides_usage()
    
                       ? (stickers.value() ? !img.second.is_sticker() : !img.second.is_emoji())
    
                       : !pack_matches))
                    continue;
    
                info.pack.images.insert(img);
    
    
            if (!info.pack.images.empty())
                infos.push_back(std::move(info));
    
        };
    
        // packs from account data
        if (auto accountpack =
              getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
            auto tmp =
              std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(&*accountpack);
            if (tmp)
                addPack(tmp->content, "", "");
        }
    
        // packs from rooms, that were enabled globally
        if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
            auto tmp = std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
              &*roomPacks);
            if (tmp) {
                for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
                    // don't add stickers from this room twice
                    if (room_id2 == room_id)
                        continue;
    
                    for (const auto &[state_id, d] : state_to_d) {
                        (void)d;
                        if (auto pack =
                              getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id2, state_id))
                            addPack(pack->content, room_id2, state_id);
    
        // packs from current room
        if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
            addPack(pack->content, room_id, "");
        }
        for (const auto &pack : getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
            addPack(pack.content, room_id, pack.state_key);
        }
    
        return infos;
    
    std::optional<mtx::events::collections::RoomAccountDataEvents>
    Cache::getAccountData(mtx::events::EventType type, const std::string &room_id)
    {
    
        auto txn = ro_txn(env_);
        return getAccountData(txn, type, room_id);
    
    std::optional<mtx::events::collections::RoomAccountDataEvents>
    Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
    {
    
        try {
            auto db = getAccountDataDb(txn, room_id);
    
            std::string_view data;
            if (db.get(txn, to_string(type), data)) {
                mtx::responses::utils::RoomAccountDataEvents events;
    
                nlohmann::json j = nlohmann::json::array({
                  nlohmann::json::parse(data),
    
                });
                mtx::responses::utils::parse_room_account_data_events(j, events);
                if (events.size() == 1)
                    return events.front();
    
        } catch (...) {
        }
        return std::nullopt;
    
    bool
    Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
                               const std::string &room_id,
                               const std::string &user_id)
    {
    
        using namespace mtx::events;
        using namespace mtx::events::state;