diff --git a/CMakeLists.txt b/CMakeLists.txt
index 78900535..6b26b2e5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -355,6 +355,7 @@ set(SRC_FILES
src/Olm.cpp
src/RegisterPage.cpp
src/SSOHandler.cpp
+ src/ImagePackModel.cpp
src/TrayIcon.cpp
src/UserSettingsPage.cpp
src/UsersModel.cpp
@@ -559,6 +560,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/MxcImageProvider.h
src/RegisterPage.h
src/SSOHandler.h
+ src/ImagePackModel.h
src/TrayIcon.h
src/UserSettingsPage.h
src/UsersModel.h
diff --git a/resources/icons/ui/sticky-note-solid.svg b/resources/icons/ui/sticky-note-solid.svg
new file mode 100644
index 00000000..bc36d474
--- /dev/null
+++ b/resources/icons/ui/sticky-note-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index 9129b154..35e5f7e7 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -8,6 +8,7 @@ import im.nheko 1.0
TextEdit {
id: r
+
textFormat: TextEdit.RichText
readOnly: true
focus: false
@@ -19,14 +20,13 @@ TextEdit {
onLinkActivated: Nheko.openLink(link)
ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink
+ Component.onCompleted: {
+ TimelineManager.fixImageRendering(r.textDocument, r);
+ }
CursorShape {
anchors.fill: parent
cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
- Component.onCompleted: {
- TimelineManager.fixImageRendering(r.textDocument, r)
- }
-
}
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 24f9b0e8..415d67a7 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
+import "./emoji"
import "./voip"
import QtQuick 2.12
import QtQuick.Controls 2.3
@@ -87,7 +88,7 @@ Rectangle {
Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
Layout.maximumHeight: Window.height / 4
Layout.minimumHeight: Settings.fontSize
- implicitWidth: inputBar.width - 4 * (22 + 16) - 24
+ implicitWidth: inputBar.width - 5 * (22 + 16) - 24
TextArea {
id: messageInput
@@ -319,6 +320,30 @@ Rectangle {
}
+ ImageButton {
+ id: stickerButton
+
+ Layout.alignment: Qt.AlignRight | Qt.AlignBottom
+ Layout.margins: 8
+ hoverEnabled: true
+ width: 22
+ height: 22
+ image: ":/icons/icons/ui/sticky-note-solid.svg"
+ ToolTip.visible: hovered
+ ToolTip.text: qsTr("Stickers")
+ onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId(), function(row) {
+ room.input.sticker(stickerPopup.model.sourceModel, row);
+ TimelineManager.focusMessageInput();
+ })
+
+ StickerPicker {
+ id: stickerPopup
+
+ colors: Nheko.colors
+ }
+
+ }
+
ImageButton {
id: emojiButton
@@ -330,7 +355,7 @@ Rectangle {
image: ":/icons/icons/ui/smile.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Emoji")
- onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
+ onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(function(emoji) {
messageInput.insert(messageInput.cursorPosition, emoji);
TimelineManager.focusMessageInput();
})
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 33dff122..f56af237 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -92,16 +92,20 @@ ScrollView {
}
}
- EmojiButton {
+ ImageButton {
id: reactButton
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
width: 16
hoverEnabled: true
+ image: ":/icons/icons/ui/smile.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
- emojiPicker: emojiPopup
- event_id: row.model ? row.model.eventId : ""
+ onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, function(emoji) {
+ var event_id = row.model ? row.model.eventId : "";
+ room.input.reaction(event_id, emoji);
+ TimelineManager.focusMessageInput();
+ })
}
ImageButton {
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 58e367a0..70db08e7 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -86,29 +86,29 @@ Item {
// fancy reply, if this is a reply
Reply {
function fromModel(role) {
- return replyTo != "" ? room.dataById(replyTo, role) : null;
+ return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null;
}
visible: replyTo
- userColor: TimelineManager.userColor(userId, Nheko.colors.base)
- blurhash: fromModel(Room.Blurhash) ?? ""
- body: fromModel(Room.Body) ?? ""
- formattedBody: fromModel(Room.FormattedBody) ?? ""
+ userColor: replyTo, TimelineManager.userColor(userId, Nheko.colors.base)
+ blurhash: replyTo, fromModel(Room.Blurhash) ?? ""
+ body: replyTo, fromModel(Room.Body) ?? ""
+ formattedBody: replyTo, fromModel(Room.FormattedBody) ?? ""
eventId: fromModel(Room.EventId) ?? ""
- filename: fromModel(Room.Filename) ?? ""
- filesize: fromModel(Room.Filesize) ?? ""
- proportionalHeight: fromModel(Room.ProportionalHeight) ?? 1
- type: fromModel(Room.Type) ?? MtxEvent.UnknownMessage
- typeString: fromModel(Room.TypeString) ?? ""
- url: fromModel(Room.Url) ?? ""
- originalWidth: fromModel(Room.OriginalWidth) ?? 0
- isOnlyEmoji: fromModel(Room.IsOnlyEmoji) ?? false
- userId: fromModel(Room.UserId) ?? ""
- userName: fromModel(Room.UserName) ?? ""
- thumbnailUrl: fromModel(Room.ThumbnailUrl) ?? ""
- roomTopic: fromModel(Room.RoomTopic) ?? ""
- roomName: fromModel(Room.RoomName) ?? ""
- callType: fromModel(Room.CallType) ?? ""
+ filename: replyTo, fromModel(Room.Filename) ?? ""
+ filesize: replyTo, fromModel(Room.Filesize) ?? ""
+ proportionalHeight: replyTo, fromModel(Room.ProportionalHeight) ?? 1
+ type: replyTo, fromModel(Room.Type) ?? MtxEvent.UnknownMessage
+ typeString: replyTo, fromModel(Room.TypeString) ?? ""
+ url: replyTo, fromModel(Room.Url) ?? ""
+ originalWidth: replyTo, fromModel(Room.OriginalWidth) ?? 0
+ isOnlyEmoji: replyTo, fromModel(Room.IsOnlyEmoji) ?? false
+ userId: replyTo, fromModel(Room.UserId) ?? ""
+ userName: replyTo, fromModel(Room.UserName) ?? ""
+ thumbnailUrl: replyTo, fromModel(Room.ThumbnailUrl) ?? ""
+ roomTopic: replyTo, fromModel(Room.RoomTopic) ?? ""
+ roomName: replyTo, fromModel(Room.RoomName) ?? ""
+ callType: replyTo, fromModel(Room.CallType) ?? ""
}
// actual message content
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
deleted file mode 100644
index 5f4d23d3..00000000
--- a/resources/qml/emoji/EmojiButton.qml
+++ /dev/null
@@ -1,23 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import "../"
-import QtQuick 2.10
-import QtQuick.Controls 2.1
-import im.nheko 1.0
-import im.nheko.EmojiModel 1.0
-
-ImageButton {
- id: emojiButton
-
- property var colors: currentActivePalette
- property var emojiPicker
- property string event_id
-
- image: ":/icons/icons/ui/smile.png"
- onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
- room.input.reaction(event_id, emoji);
- TimelineManager.focusMessageInput();
- })
-}
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index 6f10a230..354e340c 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -130,6 +130,7 @@ Menu {
boundsBehavior: Flickable.StopAtBounds
clip: true
currentIndex: -1 // prevent sorting from stealing focus
+ cacheBuffer: 500
// Individual emoji
delegate: AbstractButton {
diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml
new file mode 100644
index 00000000..813c0b12
--- /dev/null
+++ b/resources/qml/emoji/StickerPicker.qml
@@ -0,0 +1,181 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "../"
+import QtGraphicalEffects 1.0
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+import im.nheko.EmojiModel 1.0
+
+Menu {
+ id: stickerPopup
+
+ property var callback
+ property var colors
+ property string roomid
+ property alias model: gridView.model
+ property var textArea
+ property real highlightHue: Nheko.colors.highlight.hslHue
+ property real highlightSat: Nheko.colors.highlight.hslSaturation
+ property real highlightLight: Nheko.colors.highlight.hslLightness
+ readonly property int stickerDim: 128
+ readonly property int stickerDimPad: 128 + Nheko.paddingSmall
+ readonly property int stickersPerRow: 3
+
+ function show(showAt, roomid_, callback) {
+ console.debug("Showing sticker picker");
+ roomid = roomid_;
+ stickerPopup.callback = callback;
+ popup(showAt ? showAt : null);
+ }
+
+ margins: 0
+ bottomPadding: 1
+ leftPadding: 1
+ rightPadding: 1
+ modal: true
+ focus: true
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ width: stickersPerRow * stickerDimPad + 20
+
+ Rectangle {
+ color: Nheko.colors.window
+ height: columnView.implicitHeight + 4
+ width: stickersPerRow * stickerDimPad + 20
+
+ ColumnLayout {
+ id: columnView
+
+ spacing: 0
+ anchors.leftMargin: 3
+ anchors.rightMargin: 3
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 2
+
+ // Search field
+ TextField {
+ id: emojiSearch
+
+ Layout.topMargin: 3
+ Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 - 6
+ palette: Nheko.colors
+ background: null
+ placeholderTextColor: Nheko.colors.buttonText
+ color: Nheko.colors.text
+ placeholderText: qsTr("Search")
+ selectByMouse: true
+ rightPadding: clearSearch.width
+ onTextChanged: searchTimer.restart()
+ onVisibleChanged: {
+ if (visible)
+ forceActiveFocus();
+
+ }
+
+ Timer {
+ id: searchTimer
+
+ interval: 350 // tweak as needed?
+ onTriggered: stickerPopup.model.searchString = emojiSearch.text
+ }
+
+ ToolButton {
+ id: clearSearch
+
+ visible: emojiSearch.text !== ''
+ icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
+ focusPolicy: Qt.NoFocus
+ onClicked: emojiSearch.clear()
+ hoverEnabled: true
+ background: null
+
+ anchors {
+ verticalCenter: parent.verticalCenter
+ right: parent.right
+ }
+ // clear the default hover effects.
+
+ Image {
+ height: parent.height - 2 * Nheko.paddingSmall
+ width: height
+ source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
+
+ anchors {
+ verticalCenter: parent.verticalCenter
+ right: parent.right
+ margins: Nheko.paddingSmall
+ }
+
+ }
+
+ }
+
+ }
+
+ // emoji grid
+ GridView {
+ id: gridView
+
+ model: roomid ? TimelineManager.completerFor("stickers", roomid) : null
+
+ Layout.preferredHeight: cellHeight * 3.5
+ Layout.preferredWidth: stickersPerRow * stickerDimPad + 20
+ Layout.leftMargin: 4
+ cellWidth: stickerDimPad
+ cellHeight: stickerDimPad
+ boundsBehavior: Flickable.StopAtBounds
+ clip: true
+ currentIndex: -1 // prevent sorting from stealing focus
+ cacheBuffer: 500
+
+ ScrollHelper {
+ flickable: parent
+ anchors.fill: parent
+ enabled: !Settings.mobileMode
+ }
+
+ // Individual emoji
+ delegate: AbstractButton {
+ width: stickerDim
+ height: stickerDim
+ hoverEnabled: true
+ ToolTip.text: ":" + model.shortcode + ": - " + model.body
+ ToolTip.visible: hovered
+ // TODO: maybe add favorites at some point?
+ onClicked: {
+ console.debug("Picked " + model.shortcode);
+ stickerPopup.close();
+ callback(model.originalRow);
+ }
+
+ contentItem: Image {
+ height: stickerDim
+ width: stickerDim
+ source: model.url.replace("mxc://", "image://MxcImage/")
+ fillMode: Image.PreserveAspectFit
+ }
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: hovered ? Nheko.colors.highlight : 'transparent'
+ radius: 5
+ }
+
+ }
+
+ ScrollBar.vertical: ScrollBar {
+ id: emojiScroll
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 1e7d0a25..d2024eda 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -27,6 +27,7 @@
icons/ui/search@2x.png
icons/ui/settings.png
icons/ui/settings@2x.png
+ icons/ui/sticky-note-solid.svg
icons/ui/smile.png
icons/ui/smile@2x.png
icons/ui/speech-bubbles-comment-option.png
@@ -151,8 +152,8 @@
qml/ForwardCompleter.qml
qml/TypingIndicator.qml
qml/RoomSettings.qml
- qml/emoji/EmojiButton.qml
qml/emoji/EmojiPicker.qml
+ qml/emoji/StickerPicker.qml
qml/UserProfile.qml
qml/delegates/MessageDelegate.qml
qml/delegates/TextMessage.qml
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>;
using Receipts = std::map>;
@@ -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(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(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();
ref.session = unpickle(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();
+ }
+
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
+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();
+ }
+
+ 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
+Cache::getImagePacks(const std::string &room_id, bool stickers)
+{
+ auto txn = ro_txn(env_);
+ std::vector 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>(
+ &*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>(
+ &*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(
+ txn, room_id2, state_id))
+ addPack(pack->content);
+ }
+ }
+ }
+ }
+
+ // packs from current room
+ if (auto pack = getStateEvent(txn, room_id)) {
+ addPack(pack->content);
+ }
+ for (const auto &pack :
+ getStateEventsWithType(txn, room_id)) {
+ addPack(pack.content);
+ }
+
+ return infos;
+}
+
std::optional
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{});
info.seen_device_keys = j.value("seen_device_keys", std::set{});
+ info.seen_device_ids = j.value("seen_device_ids", std::set{});
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();
- msg.master_keys = obj.at("master_keys").get();
+ msg.deviceids = obj.at("deviceids").get();
}
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>();
+ msg.keys = obj.at("keys").get>();
}
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{});
+
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
+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
+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 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 deviceids;
};
struct SharedWithUsers
{
// userid to keys
- std::map keys;
+ std::map 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 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 seen_device_keys;
+ //! Device ids that were already used at least once
+ std::set 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
#include
+#include
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 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 getParentRoomIds(const std::string &room_id);
std::vector getChildRoomIds(const std::string &room_id);
+ std::vector 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 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 &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(err->status_code));
- return;
+ static_cast(err->status_code),
+ static_cast(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
+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
+
+#include
+
+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 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(row) >= images.size())
+ return {};
+ return images.at(static_cast(row)).image;
+ }
+
+private:
+ std::string room_id;
+
+ struct ImageDesc
+ {
+ QString shortcode;
+ QString packname;
+
+ mtx::events::msc2545::PackImage image;
+ };
+
+ std::vector 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::vectorquery_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::vectorinfo("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>(&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>(
&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>(&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> 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> 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 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 &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::DeviceEventinit_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,21 +887,16 @@ handle_key_request_message(const mtx::events::DeviceEventidentity_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);
- return;
- }
-
- // Check if we have the keys for the requested session.
- auto outboundSession = cache::getOutboundMegolmSession(req.content.room_id);
- if (!outboundSession.session) {
- nhlog::crypto()->warn("requested session not found in room: {}",
- req.content.room_id);
+ // 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;
}
@@ -839,7 +904,15 @@ handle_key_request_message(const mtx::events::DeviceEventidentity_keys().curve25519;
+ index.sender_key = req.content.sender_key;
+
+ // Check if we have the keys for the requested session.
+ auto sessionData = cache::getMegolmSessionData(index);
+ if (!sessionData) {
+ nhlog::crypto()->warn("requested session not found in room: {}",
+ req.content.room_id);
+ return;
+ }
const auto session = cache::getInboundMegolmSession(index);
if (!session) {
@@ -873,12 +946,12 @@ handle_key_request_message(const mtx::events::DeviceEventcurrently.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::DeviceEventidentity_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> 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 &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 &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 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"
@@ -501,6 +502,31 @@ InputBar::video(const QString &filename,
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
}
+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)
{
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
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(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
void
TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)
{
- mtx::events::RoomEvent msgCopy = {};
- msgCopy.content = content;
- msgCopy.type = eventType;
- emit newMessageToSend(msgCopy);
+ if constexpr (std::is_same_v) {
+ mtx::events::Sticker msgCopy = {};
+ msgCopy.content = content;
+ msgCopy.type = eventType;
+ emit newMessageToSend(msgCopy);
+ } else {
+ mtx::events::RoomEvent 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();
qRegisterMetaType();
qRegisterMetaType();
+ qRegisterMetaType();
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(-1) / 4);
+ stickerModel->setParent(proxy);
+ return proxy;
}
return nullptr;
}