diff --git a/src/Cache.h b/src/Cache.h
new file mode 100644
index 00000000..fa8355a5
--- /dev/null
+++ b/src/Cache.h
@@ -0,0 +1,661 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <boost/optional.hpp>
+
+#include <QDateTime>
+#include <QDir>
+#include <QImage>
+#include <QString>
+
+#include <json.hpp>
+#include <lmdb++.h>
+#include <mtx/events/join_rules.hpp>
+#include <mtx/responses.hpp>
+#include <mtxclient/crypto/client.hpp>
+#include <mutex>
+
+#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<SearchResult>)
+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<std::string>();
+ key.room_id = j.at("room_id").get<std::string>();
+}
+
+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<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions;
+ std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions;
+ std::map<std::string, OutboundGroupSessionData> 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<QString, QString> DisplayNames;
+ static QHash<QString, QString> 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<std::string> joinedRooms();
+
+ QMap<QString, RoomInfo> roomInfo(bool withInvites = true);
+ std::map<QString, bool> 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<RoomMember> 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<QString, mtx::responses::Timeline> roomMessages();
+
+ //! 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);
+
+ //! 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<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);
+
+ 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<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
+ std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms);
+ std::map<QString, RoomInfo> roomUpdates(const mtx::responses::Sync &sync)
+ {
+ return getRoomInfo(roomsWithStateUpdates(sync));
+ }
+
+ QVector<SearchResult> searchUsers(const std::string &room_id,
+ const std::string &query,
+ std::uint8_t max_items = 5);
+ std::vector<RoomSearchResult> 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<std::string> &devices);
+ std::vector<std::string> 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<std::string> getOlmSessions(const std::string &curve25519);
+ boost::optional<mtx::crypto::OlmSessionPtr> 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<class T>
+ void saveStateEvents(lmdb::txn &txn,
+ const lmdb::dbi &statesdb,
+ const lmdb::dbi &membersdb,
+ const std::string &room_id,
+ const std::vector<T> &events)
+ {
+ for (const auto &e : events)
+ saveStateEvent(txn, statesdb, membersdb, room_id, e);
+ }
+
+ template<class T>
+ 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<StateEvent<Member>>(event)) {
+ const auto e = mpark::get<StateEvent<Member>>(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<StateEvent<Encryption>>(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<class T>
+ bool isStateEvent(const T &e)
+ {
+ using namespace mtx::events;
+ using namespace mtx::events::state;
+
+ return mpark::holds_alternative<StateEvent<Aliases>>(e) ||
+ mpark::holds_alternative<StateEvent<state::Avatar>>(e) ||
+ mpark::holds_alternative<StateEvent<CanonicalAlias>>(e) ||
+ mpark::holds_alternative<StateEvent<Create>>(e) ||
+ mpark::holds_alternative<StateEvent<GuestAccess>>(e) ||
+ mpark::holds_alternative<StateEvent<HistoryVisibility>>(e) ||
+ mpark::holds_alternative<StateEvent<JoinRules>>(e) ||
+ mpark::holds_alternative<StateEvent<Name>>(e) ||
+ mpark::holds_alternative<StateEvent<Member>>(e) ||
+ mpark::holds_alternative<StateEvent<PowerLevels>>(e) ||
+ mpark::holds_alternative<StateEvent<Topic>>(e);
+ }
+
+ template<class T>
+ bool containsStateUpdates(const T &e)
+ {
+ using namespace mtx::events;
+ using namespace mtx::events::state;
+
+ return mpark::holds_alternative<StateEvent<state::Avatar>>(e) ||
+ mpark::holds_alternative<StateEvent<CanonicalAlias>>(e) ||
+ mpark::holds_alternative<StateEvent<Name>>(e) ||
+ mpark::holds_alternative<StateEvent<Member>>(e) ||
+ mpark::holds_alternative<StateEvent<Topic>>(e);
+ }
+
+ bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e)
+ {
+ using namespace mtx::events;
+ using namespace mtx::events::state;
+
+ return mpark::holds_alternative<StrippedEvent<state::Avatar>>(e) ||
+ mpark::holds_alternative<StrippedEvent<CanonicalAlias>>(e) ||
+ mpark::holds_alternative<StrippedEvent<Name>>(e) ||
+ mpark::holds_alternative<StrippedEvent<Member>>(e) ||
+ mpark::holds_alternative<StrippedEvent<Topic>>(e);
+ }
+
+ void saveInvites(lmdb::txn &txn,
+ const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
+
+ //! 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);
+ }
+ }
+
+ 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<mtx::events::state::Member> &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();
+}
|