/* * nheko Copyright (C) 2017 Konstantinos Sideris * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include "Logging.h" using mtx::events::state::JoinRule; struct RoomMember { QString user_id; QString display_name; QImage avatar; }; struct SearchResult { QString user_id; QString display_name; }; static int numeric_key_comparison(const MDB_val *a, const MDB_val *b) { auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); if (lhs < rhs) return 1; else if (lhs == rhs) return 0; return -1; } Q_DECLARE_METATYPE(SearchResult) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(RoomMember) Q_DECLARE_METATYPE(mtx::responses::Timeline) //! Used to uniquely identify a list of read receipts. struct ReadReceiptKey { std::string event_id; std::string room_id; }; inline void to_json(json &j, const ReadReceiptKey &key) { j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; } inline void from_json(const json &j, ReadReceiptKey &key) { key.event_id = j.at("event_id").get(); key.room_id = j.at("room_id").get(); } struct DescInfo { QString username; QString userid; QString body; QString timestamp; QDateTime datetime; }; //! UI info associated with a room. struct RoomInfo { //! The calculated name of the room. std::string name; //! The topic of the room. std::string topic; //! The calculated avatar url of the room. std::string avatar_url; //! Whether or not the room is an invite. bool is_invite = false; //! Total number of members in the room. int16_t member_count = 0; //! Who can access to the room. JoinRule join_rule = JoinRule::Public; bool guest_access = false; //! Metadata describing the last message in the timeline. DescInfo msgInfo; }; inline void to_json(json &j, const RoomInfo &info) { j["name"] = info.name; j["topic"] = info.topic; j["avatar_url"] = info.avatar_url; j["is_invite"] = info.is_invite; j["join_rule"] = info.join_rule; j["guest_access"] = info.guest_access; if (info.member_count != 0) j["member_count"] = info.member_count; } inline void from_json(const json &j, RoomInfo &info) { info.name = j.at("name"); info.topic = j.at("topic"); info.avatar_url = j.at("avatar_url"); info.is_invite = j.at("is_invite"); info.join_rule = j.at("join_rule"); info.guest_access = j.at("guest_access"); if (j.count("member_count")) info.member_count = j.at("member_count"); } //! Basic information per member; struct MemberInfo { std::string name; std::string avatar_url; }; inline void to_json(json &j, const MemberInfo &info) { j["name"] = info.name; j["avatar_url"] = info.avatar_url; } inline void from_json(const json &j, MemberInfo &info) { info.name = j.at("name"); info.avatar_url = j.at("avatar_url"); } struct RoomSearchResult { std::string room_id; RoomInfo info; QImage img; }; Q_DECLARE_METATYPE(RoomSearchResult) Q_DECLARE_METATYPE(RoomInfo) // Extra information associated with an outbound megolm session. struct OutboundGroupSessionData { std::string session_id; std::string session_key; uint64_t message_index = 0; }; inline void to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) { obj["session_id"] = msg.session_id; obj["session_key"] = msg.session_key; obj["message_index"] = msg.message_index; } inline void from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) { msg.session_id = obj.at("session_id"); msg.session_key = obj.at("session_key"); msg.message_index = obj.at("message_index"); } struct OutboundGroupSessionDataRef { OlmOutboundGroupSession *session; OutboundGroupSessionData data; }; struct DevicePublicKeys { std::string ed25519; std::string curve25519; }; inline void to_json(nlohmann::json &obj, const DevicePublicKeys &msg) { obj["ed25519"] = msg.ed25519; obj["curve25519"] = msg.curve25519; } inline void from_json(const nlohmann::json &obj, DevicePublicKeys &msg) { msg.ed25519 = obj.at("ed25519"); msg.curve25519 = obj.at("curve25519"); } //! Represents a unique megolm session identifier. struct MegolmSessionIndex { //! The room in which this session exists. std::string room_id; //! The session_id of the megolm session. std::string session_id; //! The curve25519 public key of the sender. std::string sender_key; //! Representation to be used in a hash map. std::string to_hash() const { return room_id + session_id + sender_key; } }; struct OlmSessionStorage { // Megolm sessions std::map group_inbound_sessions; std::map group_outbound_sessions; std::map group_outbound_session_data; // Guards for accessing megolm sessions. std::mutex group_outbound_mtx; std::mutex group_inbound_mtx; }; class Cache : public QObject { Q_OBJECT public: Cache(const QString &userId, QObject *parent = nullptr); static QHash DisplayNames; static QHash AvatarUrls; static std::string displayName(const std::string &room_id, const std::string &user_id); static QString displayName(const QString &room_id, const QString &user_id); static QString avatarUrl(const QString &room_id, const QString &user_id); static void removeDisplayName(const QString &room_id, const QString &user_id); static void removeAvatarUrl(const QString &room_id, const QString &user_id); static void insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name); static void insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); //! Load saved data for the display names & avatars. void populateMembers(); std::vector joinedRooms(); QMap roomInfo(bool withInvites = true); std::map invites(); //! Calculate & return the name of the room. QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); //! Get room join rules 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, const QString &room_id); //! Retrieve member info from a room. std::vector getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); void saveState(const mtx::responses::Sync &res); bool isInitialized() const; std::string nextBatchToken() const; 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 removeRoom(const QString &roomid) { removeRoom(roomid.toStdString()); }; void setup(); bool isFormatValid(); void setCurrentFormat(); std::map roomMessages(); //! Retrieve all the user ids from a room. std::vector 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 &eventTypes, const std::string &room_id, const std::string &user_id); //! Retrieves the saved room avatar. QImage getRoomAvatar(const QString &id); QImage getRoomAvatar(const std::string &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>; 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>; UserReceipts readReceipts(const QString &event_id, const QString &room_id); QByteArray image(const QString &url) const; QByteArray image(lmdb::txn &txn, const std::string &url) const; QByteArray image(const std::string &url) const { return image(QString::fromStdString(url)); } void saveImage(const std::string &url, const std::string &data); void saveImage(const QString &url, const QByteArray &data); RoomInfo singleRoomInfo(const std::string &room_id); std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); std::map getRoomInfo(const std::vector &rooms); std::map roomUpdates(const mtx::responses::Sync &sync) { return getRoomInfo(roomsWithStateUpdates(sync)); } QVector searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items = 5); std::vector searchRooms(const std::string &query, std::uint8_t max_items = 5); 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); //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); bool isRoomEncrypted(const std::string &room_id); //! Save the public keys for a device. void saveDeviceKeys(const std::string &device_id); void getDeviceKeys(const std::string &device_id); //! Save the device list for a user. void setDeviceList(const std::string &user_id, const std::vector &devices); std::vector getDeviceList(const std::string &user_id); // // Outbound Megolm Sessions // void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &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, int message_index); // // Inbound Megolm Sessions // void saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session); OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; // // Olm Sessions // void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); std::vector getOlmSessions(const std::string &curve25519); boost::optional getOlmSession(const std::string &curve25519, const std::string &session_id); void saveOlmAccount(const std::string &pickled); std::string restoreOlmAccount(); void restoreSessions(); OlmSessionStorage session_storage; private: //! 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); DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); void saveTimelineMessages(lmdb::txn &txn, const std::string &room_id, const mtx::responses::Timeline &res); mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); //! Remove a room from the cache. // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); template void saveStateEvents(lmdb::txn &txn, const lmdb::dbi &statesdb, const lmdb::dbi &membersdb, const std::string &room_id, const std::vector &events) { for (const auto &e : events) saveStateEvent(txn, statesdb, membersdb, room_id, e); } template void saveStateEvent(lmdb::txn &txn, const lmdb::dbi &statesdb, const lmdb::dbi &membersdb, const std::string &room_id, const T &event) { using namespace mtx::events; using namespace mtx::events::state; if (mpark::holds_alternative>(event)) { const auto e = mpark::get>(event); 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; // Lightweight representation of a member. MemberInfo tmp{display_name, e.content.avatar_url}; lmdb::dbi_put(txn, membersdb, lmdb::val(e.state_key), lmdb::val(json(tmp).dump())); insertDisplayName(QString::fromStdString(room_id), QString::fromStdString(e.state_key), QString::fromStdString(display_name)); insertAvatarUrl(QString::fromStdString(room_id), QString::fromStdString(e.state_key), QString::fromStdString(e.content.avatar_url)); break; } default: { lmdb::dbi_del( txn, membersdb, lmdb::val(e.state_key), lmdb::val("")); removeDisplayName(QString::fromStdString(room_id), QString::fromStdString(e.state_key)); removeAvatarUrl(QString::fromStdString(room_id), QString::fromStdString(e.state_key)); break; } } return; } else if (mpark::holds_alternative>(event)) { setEncryptedRoom(txn, room_id); return; } if (!isStateEvent(event)) return; mpark::visit( [&txn, &statesdb](auto e) { lmdb::dbi_put( txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); }, event); } template bool isStateEvent(const T &e) { using namespace mtx::events; using namespace mtx::events::state; return mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e); } template bool containsStateUpdates(const T &e) { using namespace mtx::events; using namespace mtx::events::state; return mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e); } bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) { using namespace mtx::events; using namespace mtx::events::state; return mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e) || mpark::holds_alternative>(e); } void saveInvites(lmdb::txn &txn, const std::map &rooms); //! Sends signals for the rooms that are removed. void removeLeftRooms(lmdb::txn &txn, const std::map &rooms) { for (const auto &room : rooms) { removeRoom(txn, room.first); // Clean up leftover invites. removeInvite(txn, room.first); } } lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) { auto db = lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); lmdb::dbi_set_compare(txn, db, numeric_key_comparison); return db; } 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 getMembersDb(lmdb::txn &txn, const std::string &room_id) { return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), 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/" + curve25519_key).c_str(), MDB_CREATE); } QString getDisplayName(const mtx::events::StateEvent &event) { if (!event.content.display_name.empty()) return QString::fromStdString(event.content.display_name); return QString::fromStdString(event.state_key); } void setNextBatchToken(lmdb::txn &txn, const std::string &token); void setNextBatchToken(lmdb::txn &txn, const QString &token); lmdb::env env_; lmdb::dbi syncStateDb_; lmdb::dbi roomsDb_; lmdb::dbi invitesDb_; lmdb::dbi mediaDb_; lmdb::dbi readReceiptsDb_; lmdb::dbi notificationsDb_; lmdb::dbi devicesDb_; lmdb::dbi deviceKeysDb_; lmdb::dbi inboundMegolmSessionDb_; lmdb::dbi outboundMegolmSessionDb_; QString localUserId_; QString cacheDirectory_; }; namespace cache { void init(const QString &user_id); Cache * client(); }