summary refs log tree commit diff
path: root/src/Cache.cc
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-04-21 16:34:50 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-04-21 16:35:03 +0300
commit2f00fc51bf27708a9c0ac1ce186043059f93923e (patch)
tree2e65159dd08fd9576b18f7f1570b41b56029ccda /src/Cache.cc
parentPopup improvements (diff)
downloadnheko-2f00fc51bf27708a9c0ac1ce186043059f93923e.tar.xz
Cache refactoring
Diffstat (limited to 'src/Cache.cc')
-rw-r--r--src/Cache.cc893
1 files changed, 677 insertions, 216 deletions
diff --git a/src/Cache.cc b/src/Cache.cc

index 3c8a1bb3..eca13635 100644 --- a/src/Cache.cc +++ b/src/Cache.cc
@@ -20,6 +20,7 @@ #include <QByteArray> #include <QDebug> #include <QFile> +#include <QHash> #include <QStandardPaths> #include <variant.hpp> @@ -27,24 +28,39 @@ #include "Cache.h" #include "RoomState.h" +//! 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("2018.01.14"); static const lmdb::val NEXT_BATCH_KEY("next_batch"); static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); +//! Cache databases and their format. +//! +//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc). +//! Format: room_id -> RoomInfo +static constexpr const char *ROOMS_DB = "rooms"; +static constexpr const char *INVITES_DB = "rooms"; +//! Keeps already downloaded media for reuse. +//! Format: matrix_url -> binary data. +static constexpr const char *MEDIA_DB = "media"; +//! Information that must be kept between sync requests. +static constexpr const char *SYNC_STATE_DB = "sync_state"; +//! Read receipts per room/event. +static constexpr const char *READ_RECEIPTS_DB = "read_receipts"; + using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} , env_{nullptr} - , stateDb_{0} - , roomDb_{0} + , syncStateDb_{0} + , roomsDb_{0} , invitesDb_{0} - , imagesDb_{0} + , mediaDb_{0} , readReceiptsDb_{0} - , isMounted_{false} - , userId_{userId} + , localUserId_{userId} {} void @@ -54,11 +70,11 @@ Cache::setup() auto statePath = QString("%1/%2/state") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(userId_.toUtf8().toHex())); + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); cacheDirectory_ = QString("%1/%2") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(userId_.toUtf8().toHex())); + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); bool isInitial = !QFile::exists(statePath); @@ -97,30 +113,26 @@ Cache::setup() } auto txn = lmdb::txn::begin(env_); - stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE); - roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE); - invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE); - imagesDb_ = lmdb::dbi::open(txn, "images", MDB_CREATE); - readReceiptsDb_ = lmdb::dbi::open(txn, "read_receipts", MDB_CREATE); - + 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); txn.commit(); - isMounted_ = true; + qRegisterMetaType<RoomInfo>(); } void Cache::saveImage(const QString &url, const QByteArray &image) { - if (!isMounted_) - return; - auto key = url.toUtf8(); try { auto txn = lmdb::txn::begin(env_); lmdb::dbi_put(txn, - imagesDb_, + mediaDb_, lmdb::val(key.data(), key.size()), lmdb::val(image.data(), image.size())); @@ -140,7 +152,7 @@ Cache::image(const QString &url) const lmdb::val image; - bool res = lmdb::dbi_get(txn, imagesDb_, lmdb::val(key.data(), key.size()), image); + bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(key.data(), key.size()), image); txn.commit(); @@ -156,163 +168,37 @@ Cache::image(const QString &url) const } void -Cache::setState(const QString &nextBatchToken, - const std::map<QString, QSharedPointer<RoomState>> &states) +Cache::removeInvite(const std::string &room_id) { - if (!isMounted_) - return; - - try { - auto txn = lmdb::txn::begin(env_); - - setNextBatchToken(txn, nextBatchToken); - - for (auto const &state : states) - insertRoomState(txn, state.first, state.second); - - txn.commit(); - } catch (const lmdb::error &e) { - qCritical() << "The cache couldn't be updated: " << e.what(); - - unmount(); - deleteData(); - } -} - -void -Cache::insertRoomState(lmdb::txn &txn, - const QString &roomid, - const QSharedPointer<RoomState> &state) -{ - auto stateEvents = state->serialize(); - auto id = roomid.toUtf8(); - - lmdb::dbi_put(txn, roomDb_, lmdb::val(id.data(), id.size()), lmdb::val(stateEvents)); - - for (const auto &membership : state->memberships) { - lmdb::dbi membersDb = - lmdb::dbi::open(txn, roomid.toStdString().c_str(), MDB_CREATE); - - // The user_id this membership event relates to, is used - // as the index on the membership database. - auto key = membership.second.state_key; - - // Serialize membership event. - nlohmann::json data = membership.second; - std::string memberEvent = data.dump(); - - switch (membership.second.content.membership) { - // We add or update (e.g invite -> join) a new user to the membership - // list. - case mtx::events::state::Membership::Invite: - case mtx::events::state::Membership::Join: { - lmdb::dbi_put(txn, membersDb, lmdb::val(key), lmdb::val(memberEvent)); - break; - } - // We remove the user from the membership list. - case mtx::events::state::Membership::Leave: - case mtx::events::state::Membership::Ban: { - lmdb::dbi_del(txn, membersDb, lmdb::val(key), lmdb::val(memberEvent)); - break; - } - case mtx::events::state::Membership::Knock: { - qWarning() - << "Skipping knock membership" << roomid << QString::fromStdString(key); - break; - } - } - } + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr); + txn.commit(); } void -Cache::removeRoom(const QString &roomid) +Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) { - if (!isMounted_) - return; - - auto txn = lmdb::txn::begin(env_, nullptr, 0); - - lmdb::dbi_del(txn, roomDb_, lmdb::val(roomid.toUtf8(), roomid.toUtf8().size()), nullptr); - - txn.commit(); + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); } void -Cache::removeInvite(const QString &room_id) +Cache::removeRoom(const std::string &roomid) { - if (!isMounted_) - return; - auto txn = lmdb::txn::begin(env_, nullptr, 0); - - lmdb::dbi_del( - txn, invitesDb_, lmdb::val(room_id.toUtf8(), room_id.toUtf8().size()), nullptr); - + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); txn.commit(); } void -Cache::states() +Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) { - std::map<QString, RoomState> states; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, roomDb_); - - std::string room; - std::string stateData; - - // Retrieve all the room names. - while (cursor.get(room, stateData, MDB_NEXT)) { - auto roomid = QString::fromUtf8(room.data(), room.size()); - auto json = nlohmann::json::parse(stateData); - - RoomState state; - state.parse(json); - - auto memberDb = lmdb::dbi::open(txn, roomid.toStdString().c_str(), MDB_CREATE); - std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> members; - - auto memberCursor = lmdb::cursor::open(txn, memberDb); - - std::string memberId; - std::string memberContent; - - while (memberCursor.get(memberId, memberContent, MDB_NEXT)) { - auto userid = QString::fromStdString(memberId); - - try { - auto data = nlohmann::json::parse(memberContent); - mtx::events::StateEvent<mtx::events::state::Member> member = data; - members.emplace(memberId, member); - } catch (std::exception &e) { - qWarning() << "Fault while parsing member event" << e.what() - << QString::fromStdString(memberContent); - continue; - } - } - - qDebug() << members.size() << "members for" << roomid; - - state.memberships = members; - states.emplace(roomid, std::move(state)); - } - - qDebug() << "Retrieved" << states.size() << "rooms"; - - cursor.close(); - - txn.commit(); - - emit statesLoaded(states); + lmdb::dbi_put(txn, syncStateDb_, NEXT_BATCH_KEY, lmdb::val(token.data(), token.size())); } void Cache::setNextBatchToken(lmdb::txn &txn, const QString &token) { - auto value = token.toUtf8(); - - lmdb::dbi_put(txn, stateDb_, NEXT_BATCH_KEY, lmdb::val(value.data(), value.size())); + setNextBatchToken(txn, token.toStdString()); } bool @@ -321,7 +207,7 @@ Cache::isInitialized() const auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val token; - bool res = lmdb::dbi_get(txn, stateDb_, NEXT_BATCH_KEY, token); + bool res = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); txn.commit(); @@ -334,7 +220,7 @@ Cache::nextBatchToken() const auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val token; - lmdb::dbi_get(txn, stateDb_, NEXT_BATCH_KEY, token); + lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); txn.commit(); @@ -356,7 +242,7 @@ Cache::isFormatValid() auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val current_version; - bool res = lmdb::dbi_get(txn, stateDb_, CACHE_FORMAT_VERSION_KEY, current_version); + bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version); txn.commit(); @@ -381,69 +267,13 @@ Cache::setCurrentFormat() lmdb::dbi_put( txn, - stateDb_, + syncStateDb_, CACHE_FORMAT_VERSION_KEY, lmdb::val(CURRENT_CACHE_FORMAT_VERSION.data(), CURRENT_CACHE_FORMAT_VERSION.size())); txn.commit(); } -std::map<std::string, mtx::responses::InvitedRoom> -Cache::invites() -{ - std::map<std::string, mtx::responses::InvitedRoom> invites; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, invitesDb_); - - std::string room_id; - std::string payload; - - mtx::responses::InvitedRoom state; - - while (cursor.get(room_id, payload, MDB_NEXT)) { - state = nlohmann::json::parse(payload); - invites.emplace(room_id, state); - } - - if (invites.size() > 0) - qDebug() << "Retrieved" << invites.size() << "invites"; - - cursor.close(); - - txn.commit(); - - return invites; -} - -void -Cache::setInvites(const std::map<std::string, mtx::responses::InvitedRoom> &invites) -{ - if (!isMounted_) - return; - - try { - auto txn = lmdb::txn::begin(env_); - - for (auto it = invites.cbegin(); it != invites.cend(); ++it) { - nlohmann::json j; - - for (const auto &e : it->second.invite_state) { - mpark::visit( - [&j](auto msg) { j["invite_state"]["events"].push_back(msg); }, - e); - } - - lmdb::dbi_put(txn, invitesDb_, lmdb::val(it->first), lmdb::val(j.dump())); - } - - txn.commit(); - } catch (const lmdb::error &e) { - qCritical() << "setInvitedRooms: " << e.what(); - unmount(); - } -} - CachedReceipts Cache::readReceipts(const QString &event_id, const QString &room_id) { @@ -533,3 +363,634 @@ Cache::updateReadReceipt(const std::string &room_id, const Receipts &receipts) } } } + +void +Cache::saveState(const mtx::responses::Sync &res) +{ + 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); + + 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(); + + lmdb::dbi_put( + txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); + } + + saveInvites(txn, res.rooms.invite); + + std::map<std::string, bool> invites; + for (const auto &invite : res.rooms.invite) + invites.emplace(std::move(invite.first), true); + + // removeStaleInvites(txn, invites); + + removeLeftRooms(txn, res.rooms.leave); + + txn.commit(); +} + +void +Cache::removeStaleInvites(lmdb::txn &txn, const std::map<std::string, bool> &curr) +{ + auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); + + std::string room_id, room_data; + while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { + if (curr.find(room_id) == curr.end()) + lmdb::cursor_del(invitesCursor); + } + + invitesCursor.close(); +} + +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; + + lmdb::dbi_put( + txn, invitesDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); + } +} + +void +Cache::saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + for (const auto &e : room.invite_state) { + if (mpark::holds_alternative<StrippedEvent<Member>>(e)) { + auto msg = mpark::get<StrippedEvent<Member>>(e); + + auto display_name = msg.content.display_name.empty() + ? msg.state_key + : msg.content.display_name; + + MemberInfo tmp{display_name, msg.content.avatar_url}; + + lmdb::dbi_put( + txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump())); + } else { + mpark::visit( + [&txn, &statesdb](auto msg) { + bool res = lmdb::dbi_put(txn, + statesdb, + lmdb::val(to_string(msg.type)), + lmdb::val(json(msg).dump())); + + if (!res) + std::cout << "couldn't save data" << json(msg).dump() + << '\n'; + }, + e); + } + } +} + +std::vector<std::string> +Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + std::vector<std::string> rooms; + for (const auto &room : res.rooms.join) { + bool hasUpdates = false; + for (const auto &s : room.second.state.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } + + for (const auto &s : room.second.timeline.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } + + if (hasUpdates) + rooms.emplace_back(room.first); + } + + for (const auto &room : res.rooms.invite) { + for (const auto &s : room.second.invite_state) { + if (containsStateUpdates(s)) { + rooms.emplace_back(room.first); + break; + } + } + } + + return rooms; +} + +std::map<QString, RoomInfo> +Cache::getRoomInfo(const std::vector<std::string> &rooms) +{ + std::map<QString, RoomInfo> room_info; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + for (const auto &room : rooms) { + lmdb::val data; + + // Check if the room is joined. + if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room), data)) { + try { + room_info.emplace( + QString::fromStdString(room), + json::parse(std::string(data.data(), data.size()))); + } catch (const json::exception &e) { + qWarning() + << "failed to parse room info:" << QString::fromStdString(room) + << QString::fromStdString(std::string(data.data(), data.size())); + } + } else { + // Check if the room is an invite. + if (lmdb::dbi_get(txn, invitesDb_, lmdb::val(room), data)) { + try { + room_info.emplace( + QString::fromStdString(room), + json::parse(std::string(data.data(), data.size()))); + } catch (const json::exception &e) { + qWarning() << "failed to parse room info for invite:" + << QString::fromStdString(room) + << QString::fromStdString( + std::string(data.data(), data.size())); + } + } + } + } + + txn.commit(); + + return room_info; +} + +QMap<QString, RoomInfo> +Cache::roomInfo() +{ + QMap<QString, RoomInfo> result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); + + std::string room_id; + std::string room_data; + + // Gather info about the joined rooms. + while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(room_data); + result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); + } + + // Gather info about the invites. + while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(room_data); + result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); + } + + invitesCursor.close(); + roomsCursor.close(); + + txn.commit(); + + return result; +} + +QString +Cache::getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); + + if (res) { + try { + StateEvent<Avatar> msg = + json::parse(std::string(event.data(), event.size())); + + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + // We don't use an avatar for group chats. + if (membersdb.size(txn) > 2) + return QString(); + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id; + std::string member_data; + + // Resolve avatar for 1-1 chats. + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo m = json::parse(member_data); + + cursor.close(); + return QString::fromStdString(m.avatar_url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + cursor.close(); + + // Default case when there is only one member. + return avatarUrl(room_id, localUserId_); +} + +QString +Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); + + if (res) { + try { + StateEvent<Name> msg = json::parse(std::string(event.data(), event.size())); + + if (!msg.content.name.empty()) + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event); + + if (res) { + try { + StateEvent<CanonicalAlias> msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.alias.empty()) + return QString::fromStdString(msg.content.alias); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + const int total = membersdb.size(txn); + + std::size_t ii = 0; + std::string user_id; + std::string member_data; + std::map<std::string, MemberInfo> members; + + while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { + try { + members.emplace(user_id, json::parse(member_data)); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + + ii++; + } + + cursor.close(); + + if (total == 1 && !members.empty()) + return QString::fromStdString(members.begin()->second.name); + + auto first_member = [&members, this]() { + for (const auto &m : members) { + if (m.first != localUserId_.toStdString()) + return QString::fromStdString(m.second.name); + } + + return localUserId_; + }(); + + if (total == 2) + return first_member; + else if (total > 2) + return QString("%1 and %2 others").arg(first_member).arg(total); + + return "Empty Room"; +} + +QString +Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); + + if (res) { + try { + StateEvent<Topic> msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.topic.empty()) + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + return QString(); +} + +QString +Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); + + if (res) { + try { + StrippedEvent<state::Name> msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id, member_data; + + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); + + return QString::fromStdString(tmp.name); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + cursor.close(); + + return QString("Empty Room"); +} + +QString +Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); + + if (res) { + try { + StrippedEvent<state::Avatar> msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id, member_data; + + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); + + return QString::fromStdString(tmp.avatar_url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + cursor.close(); + + return QString(); +} + +QString +Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = + lmdb::dbi_get(txn, db, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); + + if (res) { + try { + StrippedEvent<Topic> msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + return QString(); +} + +std::vector<std::string> +Cache::joinedRooms() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + + std::string id, data; + std::vector<std::string> room_ids; + + // Gather the room ids for the joined rooms. + while (roomsCursor.get(id, data, MDB_NEXT)) + room_ids.emplace_back(id); + + roomsCursor.close(); + txn.commit(); + + return room_ids; +} + +void +Cache::populateMembers() +{ + auto rooms = joinedRooms(); + qDebug() << "loading" << rooms.size() << "rooms"; + + auto txn = lmdb::txn::begin(env_); + + for (const auto &room : rooms) { + const auto roomid = QString::fromStdString(room); + + auto membersdb = getMembersDb(txn, room); + auto cursor = lmdb::cursor::open(txn, membersdb); + + std::string user_id, info; + while (cursor.get(user_id, info, MDB_NEXT)) { + MemberInfo m = json::parse(info); + + const auto userid = QString::fromStdString(user_id); + + insertDisplayName(roomid, userid, QString::fromStdString(m.name)); + insertAvatarUrl(roomid, userid, QString::fromStdString(m.avatar_url)); + } + + cursor.close(); + } + + txn.commit(); +} + +QVector<SearchResult> +Cache::getAutocompleteMatches(const std::string &room_id, + const std::string &query, + std::uint8_t max_items) +{ + std::multimap<int, std::pair<std::string, std::string>> items; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto cursor = lmdb::cursor::open(txn, getMembersDb(txn, room_id)); + + std::string user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + const auto display_name = displayName(room_id, user_id); + const int score = utils::levenshtein_distance(query, display_name); + + items.emplace(score, std::make_pair(user_id, display_name)); + } + + auto end = items.begin(); + + if (items.size() >= max_items) + std::advance(end, max_items); + else if (items.size() > 0) + std::advance(end, items.size()); + + QVector<SearchResult> results; + for (auto it = items.begin(); it != end; it++) { + const auto user = it->second; + results.push_back(SearchResult{QString::fromStdString(user.first), + QString::fromStdString(user.second)}); + } + + return results; +} + +QHash<QString, QString> Cache::DisplayNames; +QHash<QString, QString> Cache::AvatarUrls; + +QString +Cache::displayName(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + if (DisplayNames.contains(fmt)) + return DisplayNames[fmt]; + + return user_id; +} + +std::string +Cache::displayName(const std::string &room_id, const std::string &user_id) +{ + auto fmt = QString::fromStdString(room_id + " " + user_id); + if (DisplayNames.contains(fmt)) + return DisplayNames[fmt].toStdString(); + + return user_id; +} + +QString +Cache::avatarUrl(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + if (AvatarUrls.contains(fmt)) + return AvatarUrls[fmt]; + + return QString(); +} + +void +Cache::insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + DisplayNames.insert(fmt, display_name); +} + +void +Cache::removeDisplayName(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + DisplayNames.remove(fmt); +} + +void +Cache::insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + AvatarUrls.insert(fmt, avatar_url); +} + +void +Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + AvatarUrls.remove(fmt); +}