summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2021-07-19 14:12:28 -0400
committerJoseph Donofry <joedonofry@gmail.com>2021-07-19 14:12:28 -0400
commita4754e79d2e5d8c3a490cd01989abd25dd360646 (patch)
tree44114ff7e7527346792931e69908f02109a366ec /src
parentImport and update lurkki's branch (diff)
parentFix reaction button again (diff)
downloadnheko-a4754e79d2e5d8c3a490cd01989abd25dd360646.tar.xz
Merge remote-tracking branch 'nheko-im/master' into video_player_enhancements
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp195
-rw-r--r--src/Cache.h9
-rw-r--r--src/CacheCryptoStructs.h27
-rw-r--r--src/CacheStructs.h7
-rw-r--r--src/Cache_p.h11
-rw-r--r--src/ChatPage.cpp10
-rw-r--r--src/ImagePackModel.cpp74
-rw-r--r--src/ImagePackModel.h48
-rw-r--r--src/Olm.cpp187
-rw-r--r--src/Olm.h5
-rw-r--r--src/UserSettingsPage.cpp5
-rw-r--r--src/timeline/InputBar.cpp26
-rw-r--r--src/timeline/InputBar.h2
-rw-r--r--src/timeline/TimelineModel.cpp17
-rw-r--r--src/timeline/TimelineModel.h20
-rw-r--r--src/timeline/TimelineViewManager.cpp7
16 files changed, 532 insertions, 118 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 9304db0e..0bcf9fbf 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -78,6 +78,8 @@ constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); //! MegolmSessionIndex -> pickled OlmOutboundGroupSession constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); +//! MegolmSessionIndex -> session data about which devices have access to this +constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db"); using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; @@ -284,6 +286,7 @@ Cache::setup() // Session management inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); txn.commit(); @@ -387,9 +390,14 @@ Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) index.session_id = s.session_id; index.sender_key = s.sender_key; + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain; + if (s.sender_claimed_keys.count("ed25519")) + data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519"); + auto exported_session = mtx::crypto::import_session(s.session_key); - saveInboundMegolmSession(index, std::move(exported_session)); + saveInboundMegolmSession(index, std::move(exported_session), data); ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); } } @@ -400,7 +408,8 @@ Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) void Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session) + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data) { using namespace mtx::crypto; const auto key = json(index).dump(); @@ -420,6 +429,7 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, } inboundMegolmSessionDb_.put(txn, key, pickled); + megolmSessionDataDb_.put(txn, key, json(data).dump()); txn.commit(); } @@ -464,7 +474,7 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) void Cache::updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data_, + const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &ptr) { using namespace mtx::crypto; @@ -472,18 +482,20 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, if (!outboundMegolmSessionExists(room_id)) return; - OutboundGroupSessionData data = data_; - data.message_index = olm_outbound_group_session_message_index(ptr.get()); - data.session_id = mtx::crypto::session_id(ptr.get()); - data.session_key = mtx::crypto::session_key(ptr.get()); + GroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(ptr.get()); + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(ptr.get()); // Save the updated pickled data for the session. json j; - j["data"] = data; j["session"] = pickle<OutboundSessionObject>(ptr.get(), SECRET); auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.put(txn, room_id, j.dump()); + megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); txn.commit(); } @@ -498,24 +510,32 @@ Cache::dropOutboundMegolmSession(const std::string &room_id) { auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.del(txn, room_id); + // don't delete session data, so that we can still share the session. txn.commit(); } } void Cache::saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &session) { using namespace mtx::crypto; const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET); + GroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(session.get()); + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(session.get()); + json j; - j["data"] = data; j["session"] = pickled; auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.put(txn, room_id, j.dump()); + megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); txn.commit(); } @@ -544,8 +564,17 @@ Cache::getOutboundMegolmSession(const std::string &room_id) auto obj = json::parse(value); OutboundGroupSessionDataRef ref{}; - ref.data = obj.at("data").get<OutboundGroupSessionData>(); ref.session = unpickle<OutboundSessionObject>(obj.at("session"), SECRET); + + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(ref.session.get()); + + if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { + ref.data = nlohmann::json::parse(value).get<GroupSessionData>(); + } + return ref; } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); @@ -553,6 +582,25 @@ Cache::getOutboundMegolmSession(const std::string &room_id) } } +std::optional<GroupSessionData> +Cache::getMegolmSessionData(const MegolmSessionIndex &index) +{ + try { + using namespace mtx::crypto; + + auto txn = ro_txn(env_); + + std::string_view value; + if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { + return nlohmann::json::parse(value).get<GroupSessionData>(); + } + + return std::nullopt; + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what()); + return std::nullopt; + } +} // // OLM sessions. // @@ -829,6 +877,7 @@ Cache::deleteData() lmdb::dbi_close(env_, inboundMegolmSessionDb_); lmdb::dbi_close(env_, outboundMegolmSessionDb_); + lmdb::dbi_close(env_, megolmSessionDataDb_); env_.close(); @@ -3333,6 +3382,75 @@ Cache::getChildRoomIds(const std::string &room_id) return roomids; } +std::vector<ImagePackInfo> +Cache::getImagePacks(const std::string &room_id, bool stickers) +{ + auto txn = ro_txn(env_); + std::vector<ImagePackInfo> infos; + + auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) { + if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) { + ImagePackInfo info; + if (pack.pack) + info.packname = pack.pack->display_name; + + for (const auto &img : pack.images) { + if (img.second.overrides_usage() && + (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) + continue; + + info.images.insert(img); + } + + if (!info.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); + } + } + } + } + + // packs from current room + if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) { + addPack(pack->content); + } + for (const auto &pack : + getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) { + addPack(pack.content); + } + + return infos; +} + std::optional<mtx::events::collections::RoomAccountDataEvents> Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { @@ -3525,6 +3643,7 @@ to_json(json &j, const UserKeyCache &info) { j["device_keys"] = info.device_keys; j["seen_device_keys"] = info.seen_device_keys; + j["seen_device_ids"] = info.seen_device_ids; j["master_keys"] = info.master_keys; j["master_key_changed"] = info.master_key_changed; j["user_signing_keys"] = info.user_signing_keys; @@ -3538,6 +3657,7 @@ from_json(const json &j, UserKeyCache &info) { info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{}); info.seen_device_keys = j.value("seen_device_keys", std::set<std::string>{}); + info.seen_device_ids = j.value("seen_device_ids", std::set<std::string>{}); info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{}); info.master_key_changed = j.value("master_key_changed", false); info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{}); @@ -3634,6 +3754,15 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query keyReused = true; break; } + if (updateToWrite.seen_device_ids.count( + device_id)) { + nhlog::crypto()->warn( + "device_id '{}' reused by ({})", + device_id, + user); + keyReused = true; + break; + } } if (!keyReused && !oldDeviceKeys.count(device_id)) @@ -3644,6 +3773,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query (void)key_id; updateToWrite.seen_device_keys.insert(key); } + updateToWrite.seen_device_ids.insert(device_id); } } db.put(txn, user, json(updateToWrite).dump()); @@ -4077,17 +4207,15 @@ from_json(const json &j, MemberInfo &info) } void -to_json(nlohmann::json &obj, const DeviceAndMasterKeys &msg) +to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg) { - obj["devices"] = msg.devices; - obj["master_keys"] = msg.master_keys; + obj["deviceids"] = msg.deviceids; } void -from_json(const nlohmann::json &obj, DeviceAndMasterKeys &msg) +from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg) { - msg.devices = obj.at("devices").get<decltype(msg.devices)>(); - msg.master_keys = obj.at("master_keys").get<decltype(msg.master_keys)>(); + msg.deviceids = obj.at("deviceids").get<decltype(msg.deviceids)>(); } void @@ -4099,30 +4227,31 @@ to_json(nlohmann::json &obj, const SharedWithUsers &msg) void from_json(const nlohmann::json &obj, SharedWithUsers &msg) { - msg.keys = obj.at("keys").get<std::map<std::string, DeviceAndMasterKeys>>(); + msg.keys = obj.at("keys").get<std::map<std::string, DeviceKeysToMsgIndex>>(); } void -to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +to_json(nlohmann::json &obj, const GroupSessionData &msg) { - obj["session_id"] = msg.session_id; - obj["session_key"] = msg.session_key; obj["message_index"] = msg.message_index; obj["ts"] = msg.timestamp; - obj["initially"] = msg.initially; + obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key; + obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain; + obj["currently"] = msg.currently; } void -from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +from_json(const nlohmann::json &obj, GroupSessionData &msg) { - msg.session_id = obj.at("session_id"); - msg.session_key = obj.at("session_key"); msg.message_index = obj.at("message_index"); msg.timestamp = obj.value("ts", 0ULL); - msg.initially = obj.value("initially", SharedWithUsers{}); + msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", ""); + msg.forwarding_curve25519_key_chain = + obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{}); + msg.currently = obj.value("currently", SharedWithUsers{}); } @@ -4522,7 +4651,7 @@ isRoomMember(const std::string &user_id, const std::string &room_id) // void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { instance_->saveOutboundMegolmSession(room_id, data, session); @@ -4539,7 +4668,7 @@ outboundMegolmSessionExists(const std::string &room_id) noexcept } void updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { instance_->updateOutboundMegolmSession(room_id, data, session); @@ -4566,9 +4695,10 @@ exportSessionKeys() // void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session) + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data) { - instance_->saveInboundMegolmSession(index, std::move(session)); + instance_->saveInboundMegolmSession(index, std::move(session), data); } mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index) @@ -4580,6 +4710,11 @@ inboundMegolmSessionExists(const MegolmSessionIndex &index) { return instance_->inboundMegolmSessionExists(index); } +std::optional<GroupSessionData> +getMegolmSessionData(const MegolmSessionIndex &index) +{ + return instance_->getMegolmSessionData(index); +} // // Olm Sessions diff --git a/src/Cache.h b/src/Cache.h
index b0520f6b..57a36d73 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -200,7 +200,7 @@ isRoomMember(const std::string &user_id, const std::string &room_id); // void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); @@ -208,7 +208,7 @@ bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); @@ -223,11 +223,14 @@ exportSessionKeys(); // void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data); mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); +std::optional<GroupSessionData> +getMegolmSessionData(const MegolmSessionIndex &index); // // Olm Sessions diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 07ca274e..409c9d67 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h
@@ -27,40 +27,43 @@ enum Trust Q_ENUM_NS(Trust) } -struct DeviceAndMasterKeys +struct DeviceKeysToMsgIndex { - // map from device id or master key id to message_index - std::map<std::string, uint64_t> devices, master_keys; + // map from device key to message_index + // Using the device id is safe because we check for reuse on device list updates + // Using the device id makes our logic much easier to read. + std::map<std::string, uint64_t> deviceids; }; struct SharedWithUsers { // userid to keys - std::map<std::string, DeviceAndMasterKeys> keys; + std::map<std::string, DeviceKeysToMsgIndex> keys; }; // Extra information associated with an outbound megolm session. -struct OutboundGroupSessionData +struct GroupSessionData { - std::string session_id; - std::string session_key; uint64_t message_index = 0; uint64_t timestamp = 0; + std::string sender_claimed_ed25519_key; + std::vector<std::string> forwarding_curve25519_key_chain; + // who has access to this session. // Rotate, when a user leaves the room and share, when a user gets added. - SharedWithUsers initially, currently; + SharedWithUsers currently; }; void -to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg); +to_json(nlohmann::json &obj, const GroupSessionData &msg); void -from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg); +from_json(const nlohmann::json &obj, GroupSessionData &msg); struct OutboundGroupSessionDataRef { mtx::crypto::OutboundGroupSessionPtr session; - OutboundGroupSessionData data; + GroupSessionData data; }; struct DevicePublicKeys @@ -134,6 +137,8 @@ struct UserKeyCache bool master_key_changed = false; //! Device keys that were already used at least once std::set<std::string> seen_device_keys; + //! Device ids that were already used at least once + std::set<std::string> seen_device_ids; }; void diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index 28c70055..f274d70f 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h
@@ -11,6 +11,7 @@ #include <string> #include <mtx/events/join_rules.hpp> +#include <mtx/events/mscs/image_packs.hpp> namespace cache { enum class CacheVersion : int @@ -109,3 +110,9 @@ struct RoomSearchResult std::string room_id; RoomInfo info; }; + +struct ImagePackInfo +{ + std::string packname; + std::map<std::string, mtx::events::msc2545::PackImage> images; +}; diff --git a/src/Cache_p.h b/src/Cache_p.h
index c76cc717..13fbc371 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -225,6 +225,8 @@ public: std::vector<std::string> getParentRoomIds(const std::string &room_id); std::vector<std::string> getChildRoomIds(const std::string &room_id); + std::vector<ImagePackInfo> getImagePacks(const std::string &room_id, bool stickers); + //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); bool isRoomEncrypted(const std::string &room_id); @@ -238,12 +240,12 @@ public: // Outbound Megolm Sessions // void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); @@ -254,10 +256,12 @@ public: // Inbound Megolm Sessions // void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data); mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession( const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); + std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index); // // Olm Sessions @@ -676,6 +680,7 @@ private: lmdb::dbi inboundMegolmSessionDb_; lmdb::dbi outboundMegolmSessionDb_; + lmdb::dbi megolmSessionDataDb_; QString localUserId_; QString cacheDirectory_; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 6003eb85..10a91557 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -939,12 +939,16 @@ ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts) [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { if (err) { nhlog::crypto()->warn( - "failed to update one-time keys: {} {}", + "failed to update one-time keys: {} {} {}", err->matrix_error.error, - static_cast<int>(err->status_code)); - return; + static_cast<int>(err->status_code), + static_cast<int>(err->error_code)); + + if (err->status_code < 400 || err->status_code >= 500) + return; } + // mark as published anyway, otherwise we may end up in a loop. olm::mark_keys_as_published(); }); } diff --git a/src/ImagePackModel.cpp b/src/ImagePackModel.cpp new file mode 100644
index 00000000..9b0dca8d --- /dev/null +++ b/src/ImagePackModel.cpp
@@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ImagePackModel.h" + +#include "Cache_p.h" +#include "CompletionModelRoles.h" + +ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent) + : QAbstractListModel(parent) + , room_id(roomId) +{ + auto packs = cache::client()->getImagePacks(room_id, stickers); + + for (const auto &pack : packs) { + QString packname = QString::fromStdString(pack.packname); + + for (const auto &img : pack.images) { + ImageDesc i{}; + i.shortcode = QString::fromStdString(img.first); + i.packname = packname; + i.image = img.second; + images.push_back(std::move(i)); + } + } +} + +int +ImagePackModel::rowCount(const QModelIndex &) const +{ + return (int)images.size(); +} + +QHash<int, QByteArray> +ImagePackModel::roleNames() const +{ + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::Url, "url"}, + {Roles::ShortCode, "shortcode"}, + {Roles::Body, "body"}, + {Roles::PackName, "packname"}, + {Roles::OriginalRow, "originalRow"}, + }; +} + +QVariant +ImagePackModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + return QString::fromStdString(images[index.row()].image.url); + case Roles::Url: + return QString::fromStdString(images[index.row()].image.url); + case CompletionModel::SearchRole: + case Roles::ShortCode: + return images[index.row()].shortcode; + case CompletionModel::SearchRole2: + case Roles::Body: + return QString::fromStdString(images[index.row()].image.body); + case Roles::PackName: + return images[index.row()].packname; + case Roles::OriginalRow: + return index.row(); + default: + return {}; + } + } + return {}; +} diff --git a/src/ImagePackModel.h b/src/ImagePackModel.h new file mode 100644
index 00000000..937014ec --- /dev/null +++ b/src/ImagePackModel.h
@@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractListModel> + +#include <mtx/events/mscs/image_packs.hpp> + +class ImagePackModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + PackName, + OriginalRow, + }; + + ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + mtx::events::msc2545::PackImage imageAt(int row) + { + if (row < 0 || static_cast<size_t>(row) >= images.size()) + return {}; + return images.at(static_cast<size_t>(row)).image; + } + +private: + std::string room_id; + + struct ImageDesc + { + QString shortcode; + QString packname; + + mtx::events::msc2545::PackImage image; + }; + + std::vector<ImageDesc> images; +}; diff --git a/src/Olm.cpp b/src/Olm.cpp
index ff4c883b..18e2ddcf 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -123,7 +123,17 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) { try { olm::OlmMessage olm_msg = j_msg; - handle_olm_message(std::move(olm_msg)); + cache::client()->query_keys( + olm_msg.sender, + [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) { + if (e) { + nhlog::crypto()->error( + "Failed to query user keys, dropping olm " + "message"); + return; + } + handle_olm_message(std::move(olm_msg), userKeys); + }); } catch (const nlohmann::json::exception &e) { nhlog::crypto()->warn( "parsing error for olm message: {} {}", e.what(), j_msg.dump(2)); @@ -197,7 +207,7 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven } void -handle_olm_message(const OlmMessage &msg) +handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys) { nhlog::crypto()->info("sender : {}", msg.sender); nhlog::crypto()->info("sender_key: {}", msg.sender_key); @@ -209,7 +219,7 @@ handle_olm_message(const OlmMessage &msg) if (cipher.first != my_key) { nhlog::crypto()->debug( "Skipping message for {} since we are {}.", cipher.first, my_key); - continue; + return; } const auto type = cipher.second.type; @@ -231,6 +241,57 @@ handle_olm_message(const OlmMessage &msg) if (!payload.is_null()) { mtx::events::collections::DeviceEvents device_event; + // Other properties are included in order to prevent an attacker from + // publishing someone else's curve25519 keys as their own and subsequently + // claiming to have sent messages which they didn't. sender must correspond + // to the user who sent the event, recipient to the local user, and + // recipient_keys to the local ed25519 key. + std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"]; + if (receiver_ed25519.empty() || + receiver_ed25519 != olm::client()->identity_keys().ed25519) { + nhlog::crypto()->warn( + "Decrypted event doesn't include our ed25519: {}", + payload.dump()); + return; + } + std::string receiver = payload["recipient"]; + if (receiver.empty() || receiver != http::client()->user_id().to_string()) { + nhlog::crypto()->warn( + "Decrypted event doesn't include our user_id: {}", + payload.dump()); + return; + } + + // Clients must confirm that the sender_key and the ed25519 field value + // under the keys property match the keys returned by /keys/query for the + // given user, and must also verify the signature of the payload. Without + // this check, a client cannot be sure that the sender device owns the + // private part of the ed25519 key it claims to have in the Olm payload. + // This is crucial when the ed25519 key corresponds to a verified device. + std::string sender_ed25519 = payload["keys"]["ed25519"]; + if (sender_ed25519.empty()) { + nhlog::crypto()->warn( + "Decrypted event doesn't include sender ed25519: {}", + payload.dump()); + return; + } + + bool from_their_device = false; + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { + if (key.keys.at("curve25519:" + device_id) == msg.sender_key) { + if (key.keys.at("ed25519:" + device_id) == sender_ed25519) { + from_their_device = true; + break; + } + } + } + if (!from_their_device) { + nhlog::crypto()->warn("Decrypted event isn't sent from a device " + "listed by that user! {}", + payload.dump()); + return; + } + { std::string msg_type = payload["type"]; json event_array = json::array(); @@ -242,7 +303,7 @@ handle_olm_message(const OlmMessage &msg) if (temp_events.empty()) { nhlog::crypto()->warn("Decrypted unknown event: {}", payload.dump()); - continue; + return; } device_event = temp_events.at(0); } @@ -276,17 +337,20 @@ handle_olm_message(const OlmMessage &msg) ChatPage::instance()->receivedDeviceVerificationDone(e8->content); } else if (auto roomKey = std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) { - create_inbound_megolm_session(*roomKey, msg.sender_key); + create_inbound_megolm_session( + *roomKey, msg.sender_key, sender_ed25519); } else if (auto forwardedRoomKey = std::get_if<DeviceEvent<msg::ForwardedRoomKey>>( &device_event)) { + forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back( + msg.sender_key); import_inbound_megolm_session(*forwardedRoomKey); } else if (auto e = std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) { auto local_user = http::client()->user_id(); if (msg.sender != local_user.to_string()) - continue; + return; auto secret_name = request_id_to_secret_name.find(e->content.request_id); @@ -306,7 +370,7 @@ handle_olm_message(const OlmMessage &msg) cache::verificationStatus(local_user.to_string()); if (!verificationStatus) - continue; + return; auto deviceKeys = cache::userKeys(local_user.to_string()); std::string sender_device_id; @@ -344,7 +408,6 @@ handle_olm_message(const OlmMessage &msg) "for secrect " "'{}'", name); - return; } }); @@ -364,13 +427,8 @@ handle_olm_message(const OlmMessage &msg) } try { - auto otherUserDeviceKeys = cache::userKeys(msg.sender); - - if (!otherUserDeviceKeys) - return; - std::map<std::string, std::vector<std::string>> targets; - for (auto [device_id, key] : otherUserDeviceKeys->device_keys) { + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { if (key.keys.at("curve25519:" + device_id) == msg.sender_key) targets[msg.sender].push_back(device_id); } @@ -450,7 +508,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, std::map<std::string, std::vector<std::string>> sendSessionTo; mtx::crypto::OutboundGroupSessionPtr session = nullptr; - OutboundGroupSessionData group_session_data; + GroupSessionData group_session_data; if (cache::outboundMegolmSessionExists(room_id)) { auto res = cache::getOutboundMegolmSession(room_id); @@ -519,7 +577,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, } else { // compare devices bool device_removed = false; - for (const auto &dev : session_member_it->second.devices) { + for (const auto &dev : + session_member_it->second.deviceids) { if (!member_it->second || !member_it->second->device_keys.count( dev.first)) { @@ -541,7 +600,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, if (member_it->second) for (const auto &dev : member_it->second->device_keys) - if (!session_member_it->second.devices + if (!session_member_it->second.deviceids .count(dev.first) && (member_it->first != own_user_id || dev.first != device_id)) @@ -571,32 +630,28 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, const auto session_key = mtx::crypto::session_key(session.get()); // Saving the new megolm session. - OutboundGroupSessionData session_data{}; - session_data.session_id = mtx::crypto::session_id(session.get()); - session_data.session_key = mtx::crypto::session_key(session.get()); - session_data.message_index = 0; - session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); + GroupSessionData session_data{}; + session_data.message_index = 0; + session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); + session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519; sendSessionTo.clear(); for (const auto &[user, devices] : members) { sendSessionTo[user] = {}; - session_data.initially.keys[user] = {}; + session_data.currently.keys[user] = {}; if (devices) { for (const auto &[device_id_, key] : devices->device_keys) { (void)key; if (device_id != device_id_ || user != own_user_id) { sendSessionTo[user].push_back(device_id_); - session_data.initially.keys[user] - .devices[device_id_] = 0; + session_data.currently.keys[user] + .deviceids[device_id_] = 0; } } } } - cache::saveOutboundMegolmSession(room_id, session_data, session); - group_session_data = std::move(session_data); - { MegolmSessionIndex index; index.room_id = room_id; @@ -604,8 +659,12 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, index.sender_key = olm::client()->identity_keys().curve25519; auto megolm_session = olm::client()->init_inbound_group_session(session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session)); + cache::saveInboundMegolmSession( + index, std::move(megolm_session), session_data); } + + cache::saveOutboundMegolmSession(room_id, session_data, session); + group_session_data = std::move(session_data); } mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{}; @@ -641,8 +700,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, group_session_data.currently.keys[user] = {}; for (const auto &device_id_ : devices) { - if (!group_session_data.currently.keys[user].devices.count(device_id_)) - group_session_data.currently.keys[user].devices[device_id_] = + if (!group_session_data.currently.keys[user].deviceids.count(device_id_)) + group_session_data.currently.keys[user].deviceids[device_id_] = group_session_data.message_index; } } @@ -704,7 +763,8 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip void create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey, - const std::string &sender_key) + const std::string &sender_key, + const std::string &sender_ed25519) { MegolmSessionIndex index; index.room_id = roomKey.content.room_id; @@ -712,9 +772,13 @@ create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::R index.sender_key = sender_key; try { + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = {sender_key}; + data.sender_claimed_ed25519_key = sender_ed25519; + auto megolm_session = olm::client()->init_inbound_group_session(roomKey.content.session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session)); + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -741,7 +805,13 @@ import_inbound_megolm_session( try { auto megolm_session = olm::client()->import_inbound_group_session(roomKey.content.session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session)); + + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = + roomKey.content.forwarding_curve25519_key_chain; + data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key; + + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -817,30 +887,33 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR return; } - // Check if we were the sender of the session being requested. - if (req.content.sender_key != olm::client()->identity_keys().curve25519) { - nhlog::crypto()->debug("ignoring key request {} because we were not the sender: " - "\nrequested({}) ours({})", - req.content.request_id, - req.content.sender_key, - olm::client()->identity_keys().curve25519); + // Check if we were the sender of the session being requested (unless it is actually us + // requesting the session). + if (req.sender != http::client()->user_id().to_string() && + req.content.sender_key != olm::client()->identity_keys().curve25519) { + nhlog::crypto()->debug( + "ignoring key request {} because we did not create the requested session: " + "\nrequested({}) ours({})", + req.content.request_id, + req.content.sender_key, + olm::client()->identity_keys().curve25519); return; } + // Check that the requested session_id and the one we have saved match. + MegolmSessionIndex index{}; + index.room_id = req.content.room_id; + index.session_id = req.content.session_id; + index.sender_key = req.content.sender_key; + // Check if we have the keys for the requested session. - auto outboundSession = cache::getOutboundMegolmSession(req.content.room_id); - if (!outboundSession.session) { + auto sessionData = cache::getMegolmSessionData(index); + if (!sessionData) { nhlog::crypto()->warn("requested session not found in room: {}", req.content.room_id); return; } - // Check that the requested session_id and the one we have saved match. - MegolmSessionIndex index{}; - index.room_id = req.content.room_id; - index.session_id = req.content.session_id; - index.sender_key = olm::client()->identity_keys().curve25519; - const auto session = cache::getInboundMegolmSession(index); if (!session) { nhlog::crypto()->warn("No session with id {} in db", req.content.session_id); @@ -873,12 +946,12 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR bool shouldSeeKeys = false; uint64_t minimumIndex = -1; - if (outboundSession.data.currently.keys.count(req.sender)) { - if (outboundSession.data.currently.keys.at(req.sender) - .devices.count(req.content.requesting_device_id)) { + if (sessionData->currently.keys.count(req.sender)) { + if (sessionData->currently.keys.at(req.sender) + .deviceids.count(req.content.requesting_device_id)) { shouldSeeKeys = true; - minimumIndex = outboundSession.data.currently.keys.at(req.sender) - .devices.at(req.content.requesting_device_id); + minimumIndex = sessionData->currently.keys.at(req.sender) + .deviceids.at(req.content.requesting_device_id); } } @@ -907,8 +980,9 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR forward_key.sender_key = index.sender_key; // TODO(Nico): Figure out if this is correct - forward_key.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519; - forward_key.forwarding_curve25519_key_chain = {}; + forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key; + forward_key.forwarding_curve25519_key_chain = + sessionData->forwarding_curve25519_key_chain; send_megolm_key_to_device( req.sender, req.content.requesting_device_id, forward_key); @@ -929,6 +1003,7 @@ send_megolm_key_to_device(const std::string &user_id, std::map<std::string, std::vector<std::string>> targets; targets[user_id] = {device_id}; send_encrypted_to_device_messages(targets, room_key); + nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id); } DecryptionResult diff --git a/src/Olm.h b/src/Olm.h
index 8479f4f2..a18cbbfb 100644 --- a/src/Olm.h +++ b/src/Olm.h
@@ -59,12 +59,13 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &content); void -handle_olm_message(const OlmMessage &msg); +handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys); //! Establish a new inbound megolm session with the decrypted payload from olm. void create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey, - const std::string &sender_key); + const std::string &sender_key, + const std::string &sender_ed25519); void import_inbound_megolm_session( const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 740b8979..ffaebe61 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -91,7 +91,7 @@ UserSettings::load(std::optional<QString> profile) privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); shareKeysWithTrustedUsers_ = - settings.value("user/share_keys_with_trusted_users", true).toBool(); + settings.value("user/automatically_share_keys_with_trusted_users", false).toBool(); mobileMode_ = settings.value("user/mobile_mode", false).toBool(); emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); @@ -610,7 +610,8 @@ UserSettings::save() settings.setValue("decrypt_sidebar", decryptSidebar_); settings.setValue("privacy_screen", privacyScreen_); settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("share_keys_with_trusted_users", shareKeysWithTrustedUsers_); + settings.setValue("automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index b0747a7c..56d0d1ce 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -21,6 +21,7 @@ #include "ChatPage.h" #include "CompletionProxyModel.h" #include "Config.h" +#include "ImagePackModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -502,6 +503,31 @@ InputBar::video(const QString &filename, } void +InputBar::sticker(ImagePackModel *model, int row) +{ + if (!model || row < 0) + return; + + auto img = model->imageAt(row); + + mtx::events::msg::StickerImage sticker{}; + sticker.info = img.info.value_or(mtx::common::ImageInfo{}); + sticker.url = img.url; + sticker.body = img.body; + + if (!room->reply().isEmpty()) { + sticker.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + sticker.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); +} + +void InputBar::command(QString command, QString args) { if (command == "me") { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index c9728379..acedceb7 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h
@@ -12,6 +12,7 @@ #include <mtx/responses/messages.hpp> class TimelineModel; +class ImagePackModel; class QMimeData; class QDropEvent; class QStringList; @@ -57,6 +58,7 @@ public slots: MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); void reaction(const QString &reactedEvent, const QString &reactionKey); + void sticker(ImagePackModel *model, int row); private slots: void startTyping(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index ab11f99b..abfe28a9 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -710,6 +710,14 @@ TimelineModel::data(const QModelIndex &index, int role) const return data(*event, role); } +QVariant +TimelineModel::dataById(QString id, int role, QString relatedTo) +{ + if (auto event = events.get(id.toStdString(), relatedTo.toStdString())) + return data(*event, role); + return QVariant(); +} + bool TimelineModel::canFetchMore(const QModelIndex &) const { @@ -1292,6 +1300,14 @@ struct SendMessageVisitor sendRoomEvent<mtx::events::msg::KeyVerificationCancel, mtx::events::EventType::KeyVerificationCancel>(msg); } + void operator()(mtx::events::Sticker msg) + { + msg.type = mtx::events::EventType::Sticker; + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker); + } else + emit model_->addPendingMessageToStore(msg); + } TimelineModel *model_; }; @@ -1301,6 +1317,7 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) { std::visit( [](auto &msg) { + // gets overwritten for reactions and stickers in SendMessageVisitor msg.type = mtx::events::EventType::RoomMessage; msg.event_id = "m" + http::client()->generate_txn_id(); msg.sender = http::client()->user_id().to_string(); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index a3c973d6..0e2895d4 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -215,10 +215,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; - Q_INVOKABLE QVariant dataById(QString id, int role) - { - return data(index(idToIndex(id)), role); - } + Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo); bool canFetchMore(const QModelIndex &) const override; void fetchMore(const QModelIndex &) override; @@ -413,10 +410,17 @@ template<class T> void TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType) { - mtx::events::RoomEvent<T> msgCopy = {}; - msgCopy.content = content; - msgCopy.type = eventType; - emit newMessageToSend(msgCopy); + if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) { + mtx::events::Sticker msgCopy = {}; + msgCopy.content = content; + msgCopy.type = eventType; + emit newMessageToSend(msgCopy); + } else { + mtx::events::RoomEvent<T> msgCopy = {}; + msgCopy.content = content; + msgCopy.type = eventType; + emit newMessageToSend(msgCopy); + } resetReply(); resetEdit(); } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b39ef615..3e69f92b 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -19,6 +19,7 @@ #include "DelegateChooser.h" #include "DeviceVerificationFlow.h" #include "EventAccessors.h" +#include "ImagePackModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -144,6 +145,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qRegisterMetaType<mtx::events::msg::KeyVerificationReady>(); qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>(); qRegisterMetaType<mtx::events::msg::KeyVerificationStart>(); + qRegisterMetaType<ImagePackModel *>(); qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "im.nheko", @@ -593,6 +595,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId) auto proxy = new CompletionProxyModel(roomModel); roomModel->setParent(proxy); return proxy; + } else if (completerName == "stickers") { + auto stickerModel = new ImagePackModel(roomId.toStdString(), true); + auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4); + stickerModel->setParent(proxy); + return proxy; } return nullptr; }