From 640cfee652721a1683a0f87d409d0b85bfada211 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 11:26:01 +0200 Subject: Room: various minor tweaks and fixes --- lib/room.cpp | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6f6128e0..970fe56b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -346,6 +346,8 @@ public: QJsonObject toJson() const; + bool isLocalUser(const User* u) const { return u == q->localUser(); } + #ifdef Quotient_E2EE_ENABLED // A map from to QHash, QPair> @@ -436,8 +438,6 @@ private: template users_shortlist_t buildShortlist(const ContT& users) const; users_shortlist_t buildShortlist(const QStringList& userIds) const; - - bool isLocalUser(const User* u) const { return u == q->localUser(); } }; decltype(Room::Private::baseState) Room::Private::stubbedState {}; @@ -1432,19 +1432,7 @@ QString Room::roomMembername(const User* u) const if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) return username; // No disambiguation necessary - // Check if we can get away just attaching the bridge postfix - // (extension to the spec) - QVector bridges; - for (; namesakesIt != d->membersMap.cend() && namesakesIt.key() == username; - ++namesakesIt) { - const auto bridgeName = (*namesakesIt)->bridged(); - if (bridges.contains(bridgeName)) // Two accounts on the same bridge - return u->fullName(this); // Disambiguate fully - // Don't bother sorting, not so many bridges out there - bridges.push_back(bridgeName); - } - - return u->rawName(this); // Disambiguate using the bridge postfix only + return u->fullName(this); // Disambiguate fully } QString Room::roomMembername(const QString& userId) const @@ -2537,9 +2525,20 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return MembersChange; // clang-format off } - , [this] (const EncryptionEvent&) { - emit encryption(); // It can only be done once, so emit it here. + , [this, oldEncEvt = static_cast(oldStateEvent)]( + const EncryptionEvent&) { + // clang-format on + if (oldEncEvt + && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { + qCWarning(STATE) << "The room is already encrypted but a new" + " room encryption event arrived - ignoring"; + return NoChange; + } + // As encryption can only be switched on once, emit the signal here + // instead of aggregating and emitting in updateData() + emit encryption(); return OtherChange; + // clang-format off } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); -- cgit v1.2.3 From dd6cf808d69eaa52f7642def5f6f94500ee9bc79 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 17:12:56 +0200 Subject: User: optimise names/avatars storing and updating The current mechanism relied on a complicated and fragile machinery around setNameForRoom() and setAvatarForRoom() that maintained the "most used" entity for a given user along with "other" ones. Given that per-room avatars are pretty rare in Matrix, it's also been inefficient as kitsune-benchmark-set_ForRoom branch shows. The new mechanism stores the "default" (as per user profile) name and avatar and maintains a singleton map of avatar objects across all users. Per-user profile only (normally) exists for the local user so there's yet another inefficiency that will be fixed further down the road by introducing a separate user profile class. --- lib/room.cpp | 140 ++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 75 insertions(+), 65 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 970fe56b..ac9db318 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -192,7 +192,6 @@ public: // void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); - void renameMember(User* u, const QString& oldName); void removeMemberFromMap(const QString& username, User* u); // This updates the room displayname field (which is the way a room @@ -238,6 +237,15 @@ public: return static_cast(evt); } +// template +// const auto& getCurrentStateContent(const QString& stateKey = {}) const +// { +// if (const auto* evt = +// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr)) +// return evt->content(); +// return EventT::content_type() +// } + bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && ti->senderId() != connection->userId() @@ -1344,18 +1352,6 @@ void Room::Private::insertMemberIntoMap(User* u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, const QString& oldName) -{ - if (u->name(q) == oldName) { - qCWarning(MAIN) << "Room::Private::renameMember(): the user " - << u->fullName(q) - << "is already known in the room under a new name."; - } else if (membersMap.contains(oldName, u)) { - removeMemberFromMap(oldName, u); - insertMemberIntoMap(u); - } -} - void Room::Private::removeMemberFromMap(const QString& username, User* u) { User* namesake = nullptr; @@ -2396,14 +2392,66 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!e.isStateEvent()) return Change::NoChange; - const auto* oldStateEvent = - std::exchange(d->currentState[{ e.matrixType(), e.stateKey() }], - static_cast(&e)); + // Find a value (create empty if necessary) and get a reference to it + // getCurrentState<> is not used here because it (creates and) returns + // a stub if a value is not found, and what's needed here is a "real" event + // or nullptr. + const auto*& curStateEvent = + d->currentState[{ e.matrixType(), e.stateKey() }]; + // Prepare for the state change + visit(e, [this, oldRme = static_cast(curStateEvent)]( + const RoomMemberEvent& rme) { + auto* u = user(rme.userId()); + if (!u) { // ??? + qCCritical(MAIN) + << "Could not get a user object for" << rme.userId(); + return; + } + const auto prevMembership = oldRme ? oldRme->membership() + : MembershipType::Leave; + switch (prevMembership) { + case MembershipType::Invite: + if (rme.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + switch (rme.membership()) { + case MembershipType::Join: // rename/avatar change or no-op + if (rme.displayName() != oldRme->displayName()) { + emit memberAboutToRename(u, rme.displayName()); + d->removeMemberFromMap(u->name(this), u); + } + break; + case MembershipType::Invite: + qCWarning(MAIN) << "Membership change from Join to Invite:" + << rme; + [[fallthrough]]; + default: // whatever the new membership, it's no more Join + d->removeMemberFromMap(u->name(this), u); + emit userRemoved(u); + } + break; + default: + if (rme.membership() == MembershipType::Invite + || rme.membership() == MembershipType::Join) { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + } + }); + + // Change the state + const auto* const oldStateEvent = + std::exchange(curStateEvent, static_cast(&e)); Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); if (!is(e)) // Room member events are too numerous - qCDebug(STATE) << "Room state event:" << e; + qCDebug(STATE) << "Updated room state:" << e; + + // Update internal structures as per the change and work out the return value // clang-format off return visit(e @@ -2411,10 +2459,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return NameChange; } , [] (const RoomAliasesEvent&) { - // clang-format on - // This event has been removed by MSC-2432 - return NoChange; - // clang-format off + return NoChange; // This event has been removed by MSC2432 } , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { // clang-format on @@ -2460,63 +2505,28 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) auto* u = user(evt.userId()); const auto* oldMemberEvent = static_cast(oldStateEvent); - u->processEvent(evt, this, oldMemberEvent == nullptr); const auto prevMembership = oldMemberEvent ? oldMemberEvent->membership() : MembershipType::Leave; - if (u == localUser() && evt.membership() == MembershipType::Invite - && evt.isDirect()) - connection()->addToDirectChats(this, user(evt.senderId())); - - switch (prevMembership) { - case MembershipType::Invite: - if (evt.membership() != prevMembership) { - d->usersInvited.removeOne(u); - Q_ASSERT(!d->usersInvited.contains(u)); - } - break; - case MembershipType::Join: - if (evt.membership() == MembershipType::Invite) - qCWarning(MAIN) << "Invalid membership change from " - "Join to Invite:" - << evt; - if (evt.membership() != prevMembership) { - disconnect(u, &User::nameAboutToChange, this, nullptr); - disconnect(u, &User::nameChanged, this, nullptr); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } - break; - default: - if (evt.membership() == MembershipType::Invite - || evt.membership() == MembershipType::Join) { - d->membersLeft.removeOne(u); - Q_ASSERT(!d->membersLeft.contains(u)); - } - } - switch (evt.membership()) { case MembershipType::Join: if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); - connect(u, &User::nameAboutToChange, this, - [=](QString newName, QString, const Room* context) { - if (context == this) - emit memberAboutToRename(u, newName); - }); - connect(u, &User::nameChanged, this, - [=](QString, QString oldName, const Room* context) { - if (context == this) { - d->renameMember(u, oldName); - emit memberRenamed(u); - } - }); emit userAdded(u); + } else { + if (oldMemberEvent->displayName() != evt.displayName()) { + d->insertMemberIntoMap(u); + emit memberRenamed(u); + } + if (oldMemberEvent->avatarUrl() != evt.avatarUrl()) + emit memberAvatarChanged(u); } break; case MembershipType::Invite: if (!d->usersInvited.contains(u)) d->usersInvited.push_back(u); + if (u == localUser() && evt.isDirect()) + connection()->addToDirectChats(this, user(evt.senderId())); break; default: if (!d->membersLeft.contains(u)) -- cgit v1.2.3 From c75ef102c4b24c89f28a4773d7758adcbf4b6846 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 22 Aug 2020 21:37:42 +0200 Subject: Cleanup and some extra comments --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ac9db318..7f76d8df 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2311,7 +2311,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } } - qCDebug(STATE) << "Room" << q->objectName() << "received" + qCDebug(MESSAGES) << "Room" << q->objectName() << "received" << totalInserted << "new events; the last event is now" << timeline.back(); @@ -2326,7 +2326,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (q->readMarker(firstWriter) != timeline.crend()) { roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(STATE) + qCDebug(MESSAGES) << "Auto-promoted read marker for" << senderId << "to" << *q->readMarker(firstWriter); } -- cgit v1.2.3 From 5eff546aea4f15f7c60af40b452661d058bcabf7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 4 Sep 2020 22:46:22 +0200 Subject: Fixes of clazy warnings --- lib/room.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7f76d8df..eff3d144 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -296,8 +296,8 @@ public: void dropDuplicateEvents(RoomEvents& events) const; Changes setLastReadEvent(User* u, QString eventId); - void updateUnreadCount(rev_iter_t from, rev_iter_t to); - Changes promoteReadMarker(User* u, rev_iter_t newMarker, bool force = false); + void updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); + Changes promoteReadMarker(User* u, const rev_iter_t& newMarker, bool force = false); Changes markMessagesAsRead(rev_iter_t upToMarker); @@ -553,7 +553,7 @@ QStringList Room::localAliases() const QStringList Room::remoteAliases() const { QStringList result; - for (const auto& s : d->aliasServers) + for (const auto& s : std::as_const(d->aliasServers)) result += d->getCurrentState(s)->aliases(); return result; } @@ -640,7 +640,8 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) return Change::NoChange; } -void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) +void Room::Private::updateUnreadCount(const rev_iter_t& from, + const rev_iter_t& to) { Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); @@ -682,7 +683,8 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) } } -Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, +Room::Changes Room::Private::promoteReadMarker(User* u, + const rev_iter_t& newMarker, bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); @@ -1242,6 +1244,7 @@ QList Room::users() const { return d->membersMap.values(); } QStringList Room::memberNames() const { QStringList res; + res.reserve(d->membersMap.size()); for (auto u : qAsConst(d->membersMap)) res.append(roomMembername(u)); @@ -1686,7 +1689,7 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, uploadFile(txnId, localPath); // Below, the upload job is used as a context object to clean up connections connect(this, &Room::fileTransferCompleted, d->fileTransfers[txnId].job, - [this, txnId](const QString& id, QUrl, const QUrl& mxcUri) { + [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) { if (id == txnId) { auto it = findPendingEvent(txnId); if (it != d->unsyncedEvents.end()) { -- cgit v1.2.3 From 4b50702e014007d19cc89c2118da0af2b01b976b Mon Sep 17 00:00:00 2001 From: John Date: Tue, 27 Oct 2020 00:38:29 +0000 Subject: Added filter param to getPreviousContent so that a server side filter can be applied. --- lib/room.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index eff3d144..f8e6e6ba 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -205,7 +205,7 @@ public: /// A point in the timeline corresponding to baseState rev_iter_t timelineBase() const { return q->findInTimeline(-1); } - void getPreviousContent(int limit = 10); + void getPreviousContent(int limit = 10, const QString &filter = {}); const StateEventBase* getCurrentState(const StateEventKey& evtKey) const { @@ -1832,15 +1832,15 @@ void Room::hangupCall(const QString& callId) d->sendEvent(callId); } -void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); } +void Room::getPreviousContent(int limit, const QString &filter) { d->getPreviousContent(limit, filter); } -void Room::Private::getPreviousContent(int limit) +void Room::Private::getPreviousContent(int limit, const QString &filter) { if (isJobRunning(eventsHistoryJob)) return; eventsHistoryJob = - connection->callApi(id, prevBatch, "b", "", limit); + connection->callApi(id, prevBatch, "b", "", limit, filter); emit q->eventsHistoryJobChanged(); connect(eventsHistoryJob, &BaseJob::success, q, [=] { prevBatch = eventsHistoryJob->end(); -- cgit v1.2.3 From f4db6988bf2fd71f74ac851557d82c6f65cc89b1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 8 Nov 2020 18:57:44 +0100 Subject: More robust member profile data retrieval MemberEventContent: displayname and avatarUrl are now Omittables; CS API doesn't guarantee their presence (see also https://github.com/matrix-org/matrix-doc/issues/1375) but Quotient used to assume they are always there, causing #412. RoomMemberEvent: displayname() -> newDisplayName() and avatarUrl() -> newAvatarUrl(), to emphasise the actual semantics (and also the changed interface). The old signatures still work but are deprecated. Instead of roomMembername() (with weird camel-casing), three new methods in addition to safeMemberName() are introduced to Room: - memberName() - produces the "best known" display name for a given member; User::name() uses it to avoid the pitfall of #412. - disambiguatedMemberName() - this is what roomMembername() used to be; not recommended for direct use when UI is concerned. - safeMemberName() - remains as is, with the fix to the documentation that used to mislead that the function returns HTML-escaped content (it didn't, and doesn't). - htmlSafeMemberName() - does what safeMemberName() claimed to do. Respectively, memberNames() is deprecated in favor of safeMemberNames() and htmlSafeMemberNames(). The corresponding Q_PROPERTY uses safeMemberNames() now. Similar to memberName(), Room has got memberAvatarUrl() to spare User class from diving into Room state to find the member avatar URL. Closes #412. --- lib/room.cpp | 85 +++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 18 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f8e6e6ba..b564369c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1241,12 +1241,27 @@ QList Room::membersLeft() const { return d->membersLeft; } QList Room::users() const { return d->membersMap.values(); } -QStringList Room::memberNames() const +[[deprecated]] QStringList Room::memberNames() const +{ + return safeMemberNames(); +} + +QStringList Room::safeMemberNames() const +{ + QStringList res; + res.reserve(d->membersMap.size()); + for (auto u: std::as_const(d->membersMap)) + res.append(safeMemberName(u->id())); + + return res; +} + +QStringList Room::htmlSafeMemberNames() const { QStringList res; res.reserve(d->membersMap.size()); - for (auto u : qAsConst(d->membersMap)) - res.append(roomMembername(u)); + for (auto u: std::as_const(d->membersMap)) + res.append(htmlSafeMemberName(u->id())); return res; } @@ -1411,37 +1426,71 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, return insertedSize; } +QString Room::memberName(const QString& mxId) const +{ + // See https://github.com/matrix-org/matrix-doc/issues/1375 + const auto rme = getCurrentState(mxId); + return rme->newDisplayName() ? *rme->newDisplayName() + : rme->prevContent() ? rme->prevContent()->displayName.value_or(QString()) + : QString(); +} + QString Room::roomMembername(const User* u) const +{ + Q_ASSERT(u != nullptr); + return disambiguatedMemberName(u->id()); +} + +QString Room::roomMembername(const QString& userId) const +{ + return disambiguatedMemberName(userId); +} + +inline QString makeFullUserName(const QString& displayName, const QString& mxId) +{ + return displayName % " (" % mxId % ')'; +} + +QString Room::disambiguatedMemberName(const QString& mxId) const { // See the CS spec, section 11.2.2.3 - const auto username = u->name(this); + const auto username = memberName(mxId); if (username.isEmpty()) - return u->id(); + return mxId; auto namesakesIt = qAsConst(d->membersMap).find(username); // We expect a user to be a member of the room - but technically it is - // possible to invoke roomMemberName() even for non-members. In such case + // possible to invoke this function even for non-members. In such case // we return the full name, just in case. if (namesakesIt == d->membersMap.cend()) - return u->fullName(this); + return makeFullUserName(username, mxId); auto nextUserIt = namesakesIt; if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) return username; // No disambiguation necessary - return u->fullName(this); // Disambiguate fully + return makeFullUserName(username, mxId); // Disambiguate fully } -QString Room::roomMembername(const QString& userId) const +QString Room::safeMemberName(const QString& userId) const { - return roomMembername(user(userId)); + return sanitized(roomMembername(userId)); } -QString Room::safeMemberName(const QString& userId) const +QString Room::htmlSafeMemberName(const QString& userId) const { - return sanitized(roomMembername(userId)); + return safeMemberName(userId).toHtmlEscaped(); +} + +QUrl Room::memberAvatarUrl(const QString &mxId) const +{ + // See https://github.com/matrix-org/matrix-doc/issues/1375 + const auto rme = getCurrentState(mxId); + return rme->newAvatarUrl() ? *rme->newAvatarUrl() + : rme->prevContent() ? rme->prevContent()->avatarUrl.value_or(QUrl()) + : QUrl(); } void Room::updateData(SyncRoomData&& data, bool fromCache) @@ -2422,8 +2471,8 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) case MembershipType::Join: switch (rme.membership()) { case MembershipType::Join: // rename/avatar change or no-op - if (rme.displayName() != oldRme->displayName()) { - emit memberAboutToRename(u, rme.displayName()); + if (rme.newDisplayName()) { + emit memberAboutToRename(u, *rme.newDisplayName()); d->removeMemberFromMap(u->name(this), u); } break; @@ -2517,11 +2566,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) d->insertMemberIntoMap(u); emit userAdded(u); } else { - if (oldMemberEvent->displayName() != evt.displayName()) { + if (evt.newDisplayName()) { d->insertMemberIntoMap(u); emit memberRenamed(u); } - if (oldMemberEvent->avatarUrl() != evt.avatarUrl()) + if (evt.newAvatarUrl()) emit memberAvatarChanged(u); } break; @@ -2858,12 +2907,12 @@ MemberSorter Room::memberSorter() const { return MemberSorter(this); } bool MemberSorter::operator()(User* u1, User* u2) const { - return operator()(u1, room->roomMembername(u2)); + return operator()(u1, room->disambiguatedMemberName(u2->id())); } bool MemberSorter::operator()(User* u1, const QString& u2name) const { - auto n1 = room->roomMembername(u1); + auto n1 = room->disambiguatedMemberName(u1->id()); if (n1.startsWith('@')) n1.remove(0, 1); auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); -- cgit v1.2.3 From 538e02f92bd5ccaf4f4870deb8de1a4b93b4ee0a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 8 Nov 2020 18:58:06 +0100 Subject: Room: drop setMemberState() --- lib/room.cpp | 6 ------ 1 file changed, 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index b564369c..e98116a9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1910,12 +1910,6 @@ LeaveRoomJob* Room::leaveRoom() return connection()->leaveRoom(this); } -SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId, - const RoomMemberEvent& event) const -{ - return d->requestSetState(memberId, event.content()); -} - void Room::kickMember(const QString& memberId, const QString& reason) { connection()->callApi(id(), memberId, reason); -- cgit v1.2.3 From 4cc8838c3bbe712493a4f6ddbecd0f7093e907bb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 12 Nov 2020 18:51:35 +0100 Subject: More JSON key constants --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e98116a9..57de4f0c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2064,8 +2064,8 @@ RoomEventPtr makeRedacted(const RoomEvent& target, { auto originalJson = target.originalJsonObject(); // clang-format off - static const QStringList keepKeys { EventIdKey, TypeKey, - QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey, + static const QStringList keepKeys { + EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey, QStringLiteral("hashes"), QStringLiteral("signatures"), QStringLiteral("depth"), QStringLiteral("prev_events"), QStringLiteral("prev_state"), QStringLiteral("auth_events"), -- cgit v1.2.3 From 92a4267efc76b01d9161c204596c74e0d128cd28 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 12 Nov 2020 18:52:06 +0100 Subject: Room: add power level events to redaction rules --- lib/room.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 57de4f0c..09e3dea2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2075,16 +2075,14 @@ RoomEventPtr makeRedacted(const RoomEvent& target, std::vector> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, - { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } - // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } - // , { RoomPowerLevels::typeId(), - // { QStringLiteral("ban"), QStringLiteral("events"), - // QStringLiteral("events_default"), - // QStringLiteral("kick"), QStringLiteral("redact"), - // QStringLiteral("state_default"), QStringLiteral("users"), - // QStringLiteral("users_default") } } - , + { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }, + { RoomPowerLevelsEvent::typeId(), + { QStringLiteral("ban"), QStringLiteral("events"), + QStringLiteral("events_default"), QStringLiteral("kick"), + QStringLiteral("redact"), QStringLiteral("state_default"), + QStringLiteral("users"), QStringLiteral("users_default") } }, { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } + // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomHistoryVisibility::typeId(), // { QStringLiteral("history_visibility") } } }; -- cgit v1.2.3 From c919c021be42228ff615e581a2f80e649c992807 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 14 Nov 2020 22:34:41 +0100 Subject: Cleanup --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 09e3dea2..3b5a80a4 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1284,7 +1284,7 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED - Q_UNUSED(encryptedEvent); + Q_UNUSED(encryptedEvent) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; #else // Quotient_E2EE_ENABLED @@ -1309,8 +1309,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderKey) { #ifndef Quotient_E2EE_ENABLED - Q_UNUSED(roomKeyEvent); - Q_UNUSED(senderKey); + Q_UNUSED(roomKeyEvent) + Q_UNUSED(senderKey) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return; #else // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 52cab4b11bdd48cd87e04c01b12c698ec4145e6d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 15 Nov 2020 21:51:51 +0100 Subject: Cleanup across event classes In particular: removed unnecessary #includes, deprecated and no more used constructs, replaced stored members with dynamic generation from JSON (TypingEvent and, especially promising for performance, ReceiptEvent) --- lib/room.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 3b5a80a4..7ac3463e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2073,7 +2073,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, QStringLiteral("membership") }; // clang-format on - std::vector> keepContentKeysMap { + std::vector> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }, { RoomPowerLevelsEvent::typeId(), @@ -2621,7 +2621,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) et.start(); if (auto* evt = eventCast(event)) { d->usersTyping.clear(); - for (const QString& userId : qAsConst(evt->users())) { + for (const auto& userId : evt->users()) { auto u = user(userId); if (memberJoinState(u) == JoinState::Join) d->usersTyping.append(u); @@ -2633,7 +2633,8 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) } if (auto* evt = eventCast(event)) { int totalReceipts = 0; - for (const auto& p : qAsConst(evt->eventsWithReceipts())) { + const auto& eventsWithReceipts = evt->eventsWithReceipts(); + for (const auto& p : eventsWithReceipts) { totalReceipts += p.receipts.size(); { if (p.receipts.size() == 1) @@ -2669,11 +2670,11 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) } } } - if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 + if (eventsWithReceipts.size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" - << evt->eventsWithReceipts().size() << "event(s) with" + << eventsWithReceipts.size() << "event(s) with" << totalReceipts << "receipt(s)," << et; } return changes; -- cgit v1.2.3 From dda813899fdb4a520dc83e10c17c1923712a8f7d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 19 Nov 2020 14:56:09 +0100 Subject: Fix Q_ASSERT failure on sending messages Changes in e81117fb exposed a flaw in EncryptionEvent causing assertion failure when this event is default-initialised (i.e. no encryption). --- lib/room.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7ac3463e..1af294a7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2580,8 +2580,14 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // clang-format off } , [this, oldEncEvt = static_cast(oldStateEvent)]( - const EncryptionEvent&) { + const EncryptionEvent& ee) { // clang-format on + if (ee.algorithm().isEmpty()) { + qWarning(STATE) + << "The encryption event for room" << objectName() + << "doesn't have 'algorithm' specified - ignoring"; + return NoChange; + } if (oldEncEvt && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { qCWarning(STATE) << "The room is already encrypted but a new" -- cgit v1.2.3 From 17d6e00597fdc1c8f25808735cbc728c4a6b3506 Mon Sep 17 00:00:00 2001 From: David Faure Date: Thu, 26 Nov 2020 22:32:55 +0100 Subject: Enable QT_NO_URL_CAST_FROM_STRING and QT_STRICT_ITERATORS. * QT_NO_URL_CAST_FROM_STRING makes it clearer where QUrls are created from QStrings (which incurs a parsing cost). * QT_STRICT_ITERATORS helps detecting where begin()/end() is used instead of cbegin()/cend(). KDE developers have verified that the generated assembly code is identical. --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1af294a7..a309cd24 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1947,7 +1947,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, }); connect(job, &BaseJob::success, this, [this, id, localFilename, job] { d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, job->contentUri()); + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); -- cgit v1.2.3 From ff020f3b7b95bbdcff7c98c54b84a6d8de38c149 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 26 Nov 2020 10:45:30 +0100 Subject: Room: fix breakage in internal member map The change in 39830496 led to prev_content becoming a fallback not only for displaying user names but also for storing them in the internal member map, which is really not what was intended. A lot of debug logging has been added - this will be moved to a new logging category before merging. --- lib/room.cpp | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1af294a7..5992a3ae 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -192,7 +192,7 @@ public: // void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); - void removeMemberFromMap(const QString& username, User* u); + void removeMemberFromMap(User* u); // This updates the room displayname field (which is the way a room // should be shown in the room list); called whenever the list of @@ -1354,13 +1354,22 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) void Room::Private::insertMemberIntoMap(User* u) { - const auto userName = u->name(q); + const auto userName = + getCurrentState(u->id())->displayName(); + qDebug(STATE) << "insertMemberIntoMap(), user" << u->id() << "with name" + << userName; // If there is exactly one namesake of the added user, signal member // renaming for that other one because the two should be disambiguated now. const auto namesakes = membersMap.values(userName); + qDebug(STATE) << namesakes.size() << "namesake(s) found"; // Callers should check they are not adding an existing user once more. Q_ASSERT(!namesakes.contains(u)); + if (namesakes.contains(u)) { // Release version whines but continues + qCCritical(STATE) << "Trying to add a user" << u->id() << "to room" + << q->objectName() << "but that's already in it"; + return; + } if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), @@ -1370,16 +1379,22 @@ void Room::Private::insertMemberIntoMap(User* u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::removeMemberFromMap(const QString& username, User* u) +void Room::Private::removeMemberFromMap(User* u) { + const auto userName = + getCurrentState(u->id())->displayName(); + + qDebug(STATE) << "removeMemberFromMap(), username" << userName << "for user" + << u->id(); User* namesake = nullptr; - auto namesakes = membersMap.values(username); + auto namesakes = membersMap.values(userName); if (namesakes.size() == 2) { namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); - emit q->memberAboutToRename(namesake, username); + emit q->memberAboutToRename(namesake, userName); } - membersMap.remove(username, u); + const auto removed = membersMap.remove(userName, u); + qDebug(STATE) << "Removed" << removed << "entries"; // If there was one namesake besides the removed user, signal member // renaming for it because it doesn't need to be disambiguated any more. if (namesake) @@ -2465,7 +2480,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) case MembershipType::Join: // rename/avatar change or no-op if (rme.newDisplayName()) { emit memberAboutToRename(u, *rme.newDisplayName()); - d->removeMemberFromMap(u->name(this), u); + d->removeMemberFromMap(u); } break; case MembershipType::Invite: @@ -2473,7 +2488,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) << rme; [[fallthrough]]; default: // whatever the new membership, it's no more Join - d->removeMemberFromMap(u->name(this), u); + d->removeMemberFromMap(u); emit userRemoved(u); } break; @@ -2555,11 +2570,13 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) switch (evt.membership()) { case MembershipType::Join: if (prevMembership != MembershipType::Join) { + qDebug(STATE) << "!Join -> Join"; d->insertMemberIntoMap(u); emit userAdded(u); } else { if (evt.newDisplayName()) { - d->insertMemberIntoMap(u); + qDebug(STATE) << "After renaming"; + d->insertMemberIntoMap(u); emit memberRenamed(u); } if (evt.newAvatarUrl()) @@ -2779,7 +2796,7 @@ QString Room::Private::calculateDisplayname() const const bool localUserIsIn = joinState == JoinState::Join; const bool emptyRoom = membersMap.isEmpty() - || (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); + || (membersMap.size() == 1 && isLocalUser(*membersMap.cbegin())); const bool nonEmptySummary = summary.heroes && !summary.heroes->empty(); auto shortlist = nonEmptySummary ? buildShortlist(*summary.heroes) : !emptyRoom ? buildShortlist(membersMap) -- cgit v1.2.3 From e617f0151df9a5edbefeb2c36d306a2989a278af Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 24 Dec 2020 18:20:04 +0100 Subject: Fix clang-tidy/clazy warnings (cherry picked from commit 0a2acd750a4155969092be674ed3dd9a71b2354f) --- lib/room.cpp | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 5992a3ae..55e2931e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -350,7 +350,7 @@ public: */ bool processReplacement(const RoomMessageEvent& newEvent); - void setTags(TagsMap newTags); + void setTags(TagsMap&& newTags); QJsonObject toJson() const; @@ -1073,11 +1073,11 @@ void Room::setTags(TagsMap newTags, ActionScope applyOn) if (propagate) { for (auto* r = this; (r = r->successor(joinStates));) - r->setTags(newTags, ActionScope::ThisRoomOnly); + r->setTags(d->tags, ActionScope::ThisRoomOnly); } } -void Room::Private::setTags(TagsMap newTags) +void Room::Private::setTags(TagsMap&& newTags) { emit q->tagsAboutToChange(); const auto keys = newTags.keys(); @@ -1192,8 +1192,8 @@ QString Room::fileNameToDownload(const QString& eventId) const FileTransferInfo Room::fileTransferInfo(const QString& id) const { - auto infoIt = d->fileTransfers.find(id); - if (infoIt == d->fileTransfers.end()) + const auto infoIt = d->fileTransfers.constFind(id); + if (infoIt == d->fileTransfers.cend()) return {}; // FIXME: Add lib tests to make sure FileTransferInfo::status stays @@ -1222,8 +1222,8 @@ QUrl Room::fileSource(const QString& id) const return url; // No urlToDownload means it's a pending or completed upload. - auto infoIt = d->fileTransfers.find(id); - if (infoIt != d->fileTransfers.end()) + auto infoIt = d->fileTransfers.constFind(id); + if (infoIt != d->fileTransfers.cend()) return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); qCWarning(MAIN) << "File source for identifier" << id << "not found"; @@ -1312,7 +1312,6 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, Q_UNUSED(roomKeyEvent) Q_UNUSED(senderKey) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; - return; #else // Quotient_E2EE_ENABLED if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { qCWarning(E2EE) << "Ignoring unsupported algorithm" @@ -1438,7 +1437,7 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, } const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); - return insertedSize; + return Timeline::size_type(insertedSize); } QString Room::memberName(const QString& mxId) const @@ -1654,8 +1653,8 @@ QString Room::retryMessage(const QString& txnId) const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); qCDebug(EVENTS) << "Retrying transaction" << txnId; - const auto& transferIt = d->fileTransfers.find(txnId); - if (transferIt != d->fileTransfers.end()) { + const auto& transferIt = d->fileTransfers.constFind(txnId); + if (transferIt != d->fileTransfers.cend()) { Q_ASSERT(transferIt->isUpload); if (transferIt->status == FileTransferInfo::Completed) { qCDebug(MESSAGES) @@ -1752,7 +1751,8 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, // to enable the preview while the event is pending. uploadFile(txnId, localPath); // Below, the upload job is used as a context object to clean up connections - connect(this, &Room::fileTransferCompleted, d->fileTransfers[txnId].job, + const auto& transferJob = d->fileTransfers.value(txnId).job; + connect(this, &Room::fileTransferCompleted, transferJob, [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) { if (id == txnId) { auto it = findPendingEvent(txnId); @@ -1771,7 +1771,7 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, } } }); - connect(this, &Room::fileTransferCancelled, d->fileTransfers[txnId].job, + connect(this, &Room::fileTransferCancelled, transferJob, [this, txnId](const QString& id) { if (id == txnId) { auto it = findPendingEvent(txnId); @@ -1973,8 +1973,8 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, void Room::downloadFile(const QString& eventId, const QUrl& localFilename) { - auto ongoingTransfer = d->fileTransfers.find(eventId); - if (ongoingTransfer != d->fileTransfers.end() + if (auto ongoingTransfer = d->fileTransfers.constFind(eventId); + ongoingTransfer != d->fileTransfers.cend() && ongoingTransfer->status == FileTransferInfo::Started) { qCWarning(MAIN) << "Transfer for" << eventId << "is ongoing; download won't start"; @@ -2031,8 +2031,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) void Room::cancelFileTransfer(const QString& id) { - auto it = d->fileTransfers.find(id); - if (it == d->fileTransfers.end()) { + const auto it = d->fileTransfers.constFind(id); + if (it == d->fileTransfers.cend()) { qCWarning(MAIN) << "No information on file transfer" << id << "in room" << d->id; return; @@ -2134,8 +2134,8 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.find(redaction.redactedEvent()); - if (pIdx == eventsIndex.end()) + const auto pIdx = eventsIndex.constFind(redaction.redactedEvent()); + if (pIdx == eventsIndex.cend()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); @@ -2205,8 +2205,8 @@ bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.find(newEvent.replacedEvent()); - if (pIdx == eventsIndex.end()) + const auto pIdx = eventsIndex.constFind(newEvent.replacedEvent()); + if (pIdx == eventsIndex.cend()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); @@ -2451,12 +2451,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!e.isStateEvent()) return Change::NoChange; - // Find a value (create empty if necessary) and get a reference to it - // getCurrentState<> is not used here because it (creates and) returns + // Find a value (create an empty one if necessary) and get a reference + // to it. Can't use getCurrentState<>() because it (creates and) returns // a stub if a value is not found, and what's needed here is a "real" event // or nullptr. - const auto*& curStateEvent = - d->currentState[{ e.matrixType(), e.stateKey() }]; + auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change visit(e, [this, oldRme = static_cast(curStateEvent)]( const RoomMemberEvent& rme) { -- cgit v1.2.3 From ecaf0093e5857074b51607924035555a4442d4d1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 24 Dec 2020 18:21:41 +0100 Subject: Room: don't accept . at 0-th position in the tag Also: use a structured binding for better code readability. (cherry picked from commit 66972c81d018231f08f3767feda4b41ae5e1b8e0) --- lib/room.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 55e2931e..a19624d8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1010,11 +1010,11 @@ TagRecord Room::tag(const QString& name) const { return d->tags.value(name); } std::pair validatedTag(QString name) { - if (name.contains('.')) + if (name.isEmpty() || name.indexOf('.', 1) != -1) return { false, name }; qCWarning(MAIN) << "The tag" << name - << "doesn't follow the CS API conventions"; + << "doesn't follow the CS API conventions"; name.prepend("u."); qCWarning(MAIN) << "Using " << name << "instead"; @@ -1082,11 +1082,11 @@ void Room::Private::setTags(TagsMap&& newTags) emit q->tagsAboutToChange(); const auto keys = newTags.keys(); for (const auto& k : keys) - if (const auto& checkRes = validatedTag(k); checkRes.first) { - if (newTags.contains(checkRes.second)) + if (const auto& [adjusted, adjustedTag] = validatedTag(k); adjusted) { + if (newTags.contains(adjustedTag)) newTags.remove(k); else - newTags.insert(checkRes.second, newTags.take(k)); + newTags.insert(adjustedTag, newTags.take(k)); } tags = move(newTags); -- cgit v1.2.3 From cd9c9296bb1ac7af7ebbbf66931e731dbf581bc8 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 26 Dec 2020 14:54:31 +0100 Subject: Port existing copyright statement to reuse using licensedigger --- lib/room.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a19624d8..155f5cd9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach + * SPDX-FileCopyrightText: 2015 Felix Rohrbach * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "room.h" -- cgit v1.2.3 From 56c1db077b5da653c230432abc6c746318a77bed Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 23:31:20 +0100 Subject: Cleanup and clang-tidy/clazy fixes --- lib/room.cpp | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a19624d8..602653f3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2522,31 +2522,20 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { // clang-format on setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); - QString previousCanonicalAlias = - oldStateEvent - ? static_cast(oldStateEvent) - ->alias() - : QString(); - - auto previousAltAliases = - oldStateEvent - ? static_cast(oldStateEvent) - ->altAliases() - : QStringList(); - - if (!previousCanonicalAlias.isEmpty()) { - previousAltAliases.push_back(previousCanonicalAlias); + const auto* oldCae = + static_cast(oldStateEvent); + QStringList previousAltAliases {}; + if (oldCae) { + previousAltAliases = oldCae->altAliases(); + if (!oldCae->alias().isEmpty()) + previousAltAliases.push_back(oldCae->alias()); } - const auto previousAliases = std::move(previousAltAliases); - auto newAliases = cae.altAliases(); - - if (!cae.alias().isEmpty()) { + if (!cae.alias().isEmpty()) newAliases.push_front(cae.alias()); - } - connection()->updateRoomAliases(id(), previousAliases, newAliases); + connection()->updateRoomAliases(id(), previousAltAliases, newAliases); return AliasesChange; // clang-format off } @@ -2588,7 +2577,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (u == localUser() && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); break; - default: + case MembershipType::Knock: + case MembershipType::Ban: + case MembershipType::Leave: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); } -- cgit v1.2.3 From 4bdeacdd332865abf55b94af29f100842ab5d04f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 28 Dec 2020 11:18:05 +0100 Subject: Use generated SetReadMarkerJob instead of PostReadMarkersJob --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 602653f3..f127b42b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -36,6 +36,7 @@ #include "csapi/room_state.h" #include "csapi/room_upgrades.h" #include "csapi/rooms.h" +#include "csapi/read_markers.h" #include "csapi/tags.h" #include "events/callanswerevent.h" @@ -55,7 +56,6 @@ #include "events/roompowerlevelsevent.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" -#include "jobs/postreadmarkersjob.h" #include "events/roomcanonicalaliasevent.h" #include @@ -632,8 +632,8 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) emit q->readMarkerForUserMoved(u, eventId, storedId); if (isLocalUser(u)) { if (storedId != serverReadMarker) - connection->callApi(BackgroundRequest, id, - storedId); + connection->callApi(BackgroundRequest, id, + storedId); emit q->readMarkerMoved(eventId, storedId); return Change::ReadMarkerChange; } -- cgit v1.2.3 From dcef98f962c29b004d5d9fff1cff0102c6c9768f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 28 Dec 2020 15:53:31 +0100 Subject: Room: really fix namesakes assertion failure This removes now-deprecated RoomMemberEvent API usages and also does a few more things differently from the stable branch. Rather than rely on prev_content (the way pre-0.7 goes), processStateEvent() now includes a pre-check for no-op state events (the fix in ff020f3b turned out to be insufficient for such events and they still caused the same assertion failure at some point down the line). Now the state event is only added to currentState and, where relevant, to baseState, if it actually changes the room state; otherwise, it is ignored for the purpose of state tracking (even when still added to the timeline, if it came in the timeline block). One side-effect of this change is that processStateEvent() now returns OtherChange instead of NoChange for unknown state events. At the same time removeMemberFromMap() now has an additional safety net, making sure that a given user is actually deleted from the map even if their name is mismatched. This comes at a cost of looking through the whole hashmap but normally should not occur with the current code that shaves away no-op state events. We'll only see when some client starts to actively use 0.7 (quotest doesn't trigger those). --- lib/room.cpp | 216 ++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 134 insertions(+), 82 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f127b42b..5346c4ff 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -263,10 +263,11 @@ public: for (auto&& eptr : events) { const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); - // Update baseState afterwards to make sure that the old state - // is valid and usable inside processStateEvent - changes |= q->processStateEvent(evt); - baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); + auto change = q->processStateEvent(evt); + if (change != NoChange) { + changes |= change; + baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); + } } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) @@ -1353,23 +1354,27 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) void Room::Private::insertMemberIntoMap(User* u) { - const auto userName = - getCurrentState(u->id())->displayName(); - qDebug(STATE) << "insertMemberIntoMap(), user" << u->id() << "with name" - << userName; - // If there is exactly one namesake of the added user, signal member - // renaming for that other one because the two should be disambiguated now. + const auto maybeUserName = + getCurrentState(u->id())->newDisplayName(); + if (!maybeUserName) + qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() + << "has no name (even empty)"; + const auto userName = maybeUserName.value_or(QString()); const auto namesakes = membersMap.values(userName); - qDebug(STATE) << namesakes.size() << "namesake(s) found"; + qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id() + << "with name" << userName << '-' + << namesakes.size() << "namesake(s) found"; - // Callers should check they are not adding an existing user once more. + // Callers should make sure they are not adding an existing user once more Q_ASSERT(!namesakes.contains(u)); if (namesakes.contains(u)) { // Release version whines but continues - qCCritical(STATE) << "Trying to add a user" << u->id() << "to room" - << q->objectName() << "but that's already in it"; + qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room" + << q->objectName() << "but that's already in it"; return; } + // If there is exactly one namesake of the added user, signal member + // renaming for that other one because the two should be disambiguated now if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -1381,21 +1386,41 @@ void Room::Private::insertMemberIntoMap(User* u) void Room::Private::removeMemberFromMap(User* u) { const auto userName = - getCurrentState(u->id())->displayName(); + getCurrentState( + u->id())->newDisplayName().value_or(QString()); - qDebug(STATE) << "removeMemberFromMap(), username" << userName << "for user" - << u->id(); + qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName + << "for user" << u->id(); User* namesake = nullptr; auto namesakes = membersMap.values(userName); + // If there was one namesake besides the removed user, signal member + // renaming for it because it doesn't need to be disambiguated any more. if (namesakes.size() == 2) { - namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); + namesake = + namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); emit q->memberAboutToRename(namesake, userName); } const auto removed = membersMap.remove(userName, u); - qDebug(STATE) << "Removed" << removed << "entries"; - // If there was one namesake besides the removed user, signal member - // renaming for it because it doesn't need to be disambiguated any more. + if (removed == 0) { + qCDebug(MEMBERS) << "No entries removed; checking the whole list"; + // Unless at the stage of initial filling, this no removed entries + // is suspicious; double-check that this user is not found in + // the whole map, and stop (for debug builds) or shout in the logs + // (for release builds) if there's one. That search is O(n), which + // may come rather expensive for larger rooms. + QElapsedTimer et; + auto it = std::find(membersMap.cbegin(), membersMap.cend(), u); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(MEMBERS) << "...done in" << et; + if (it != membersMap.cend()) { + Q_ASSERT_X(false, __FUNCTION__, + "Mismatched name in the room members list"); + qCCritical(MEMBERS) << "Mismatched name in the room members list;" + " avoiding the list corruption"; + membersMap.remove(it.key(), u); + } + } if (namesake) emit q->memberRenamed(namesake); } @@ -2449,7 +2474,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) Room::Changes Room::processStateEvent(const RoomEvent& e) { if (!e.isStateEvent()) - return Change::NoChange; + return NoChange; // Find a value (create an empty one if necessary) and get a reference // to it. Can't use getCurrentState<>() because it (creates and) returns @@ -2457,48 +2482,87 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // or nullptr. auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change - visit(e, [this, oldRme = static_cast(curStateEvent)]( - const RoomMemberEvent& rme) { - auto* u = user(rme.userId()); - if (!u) { // ??? - qCCritical(MAIN) - << "Could not get a user object for" << rme.userId(); - return; - } - const auto prevMembership = oldRme ? oldRme->membership() - : MembershipType::Leave; - switch (prevMembership) { - case MembershipType::Invite: - if (rme.membership() != prevMembership) { - d->usersInvited.removeOne(u); - Q_ASSERT(!d->usersInvited.contains(u)); + // clang-format off + const bool proceed = visit(e + , [this, curStateEvent](const RoomMemberEvent& rme) { + // clang-format on + auto* oldRme = static_cast(curStateEvent); + auto* u = user(rme.userId()); + if (!u) { // Some terribly malformed user id? + qCCritical(MAIN) << "Could not get a user object for" + << rme.userId(); + return false; // Stay low and hope for the best... } - break; - case MembershipType::Join: - switch (rme.membership()) { - case MembershipType::Join: // rename/avatar change or no-op - if (rme.newDisplayName()) { - emit memberAboutToRename(u, *rme.newDisplayName()); + const auto prevMembership = oldRme ? oldRme->membership() + : MembershipType::Leave; + switch (prevMembership) { + case MembershipType::Invite: + if (rme.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + if (rme.membership() == MembershipType::Join) { + // rename/avatar change or no-op + if (rme.newDisplayName()) { + emit memberAboutToRename(u, *rme.newDisplayName()); + d->removeMemberFromMap(u); + } + if (!rme.newDisplayName() && !rme.newAvatarUrl()) { + qCWarning(MEMBERS) + << "No-op membership event for" << rme.userId() + << "- retaining the state"; + qCWarning(MEMBERS) << "The event dump:" << rme; + return false; + } + } else { + if (rme.membership() == MembershipType::Invite) + qCWarning(MAIN) + << "Membership change from Join to Invite:" << rme; + // whatever the new membership, it's no more Join d->removeMemberFromMap(u); + emit userRemoved(u); } break; - case MembershipType::Invite: - qCWarning(MAIN) << "Membership change from Join to Invite:" - << rme; - [[fallthrough]]; - default: // whatever the new membership, it's no more Join - d->removeMemberFromMap(u); - emit userRemoved(u); + case MembershipType::Ban: + case MembershipType::Knock: + case MembershipType::Leave: + if (rme.membership() == MembershipType::Invite + || rme.membership() == MembershipType::Join) { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + break; + case MembershipType::Undefined: + ; // A warning will be dropped in the post-processing block below } - break; - default: - if (rme.membership() == MembershipType::Invite - || rme.membership() == MembershipType::Join) { - d->membersLeft.removeOne(u); - Q_ASSERT(!d->membersLeft.contains(u)); + return true; + // clang-format off + } + , [this, curStateEvent]( const EncryptionEvent& ee) { + // clang-format on + auto* oldEncEvt = + static_cast(curStateEvent); + if (ee.algorithm().isEmpty()) { + qWarning(STATE) + << "The encryption event for room" << objectName() + << "doesn't have 'algorithm' specified - ignoring"; + return false; + } + if (oldEncEvt + && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { + qCWarning(STATE) << "The room is already encrypted but a new" + " room encryption event arrived - ignoring"; + return false; } + return true; + // clang-format off } - }); + , true); // By default, go forward with the state change + // clang-format on + if (!proceed) + return NoChange; // Change the state const auto* const oldStateEvent = @@ -2506,19 +2570,18 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); - if (!is(e)) // Room member events are too numerous + if (is(e)) + qCDebug(MEMBERS) << "Updated room member state:" << e; + else qCDebug(STATE) << "Updated room state:" << e; // Update internal structures as per the change and work out the return value // clang-format off - return visit(e + const auto result = visit(e , [] (const RoomNameEvent&) { return NameChange; } - , [] (const RoomAliasesEvent&) { - return NoChange; // This event has been removed by MSC2432 - } , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { // clang-format on setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); @@ -2558,13 +2621,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) switch (evt.membership()) { case MembershipType::Join: if (prevMembership != MembershipType::Join) { - qDebug(STATE) << "!Join -> Join"; d->insertMemberIntoMap(u); emit userAdded(u); } else { if (evt.newDisplayName()) { - qDebug(STATE) << "After renaming"; - d->insertMemberIntoMap(u); + d->insertMemberIntoMap(u); emit memberRenamed(u); } if (evt.newAvatarUrl()) @@ -2582,30 +2643,18 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) case MembershipType::Leave: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); + break; + case MembershipType::Undefined: + qCWarning(MEMBERS) << "Ignored undefined membership type"; } return MembersChange; // clang-format off } - , [this, oldEncEvt = static_cast(oldStateEvent)]( - const EncryptionEvent& ee) { - // clang-format on - if (ee.algorithm().isEmpty()) { - qWarning(STATE) - << "The encryption event for room" << objectName() - << "doesn't have 'algorithm' specified - ignoring"; - return NoChange; - } - if (oldEncEvt - && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { - qCWarning(STATE) << "The room is already encrypted but a new" - " room encryption event arrived - ignoring"; - return NoChange; - } + , [this] (const EncryptionEvent&) { // As encryption can only be switched on once, emit the signal here // instead of aggregating and emitting in updateData() emit encryption(); return OtherChange; - // clang-format off } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); @@ -2622,9 +2671,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) }); return OtherChange; + // clang-format off } - ); + , OtherChange); // clang-format on + Q_ASSERT(result != NoChange); + return result; } Room::Changes Room::processEphemeralEvent(EventPtr&& event) -- cgit v1.2.3 From a49c3f2eb4e1aa5c6687c7637c1a06fcd69b0a23 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 28 Dec 2020 16:02:00 +0100 Subject: Room: inline a once-used variable --- lib/room.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 5346c4ff..a9b2ba30 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1401,8 +1401,7 @@ void Room::Private::removeMemberFromMap(User* u) Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); emit q->memberAboutToRename(namesake, userName); } - const auto removed = membersMap.remove(userName, u); - if (removed == 0) { + if (membersMap.remove(userName, u) == 0) { qCDebug(MEMBERS) << "No entries removed; checking the whole list"; // Unless at the stage of initial filling, this no removed entries // is suspicious; double-check that this user is not found in -- cgit v1.2.3 From 78a3137920d9680072dc3796dd90f849e8467fd4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 7 Jan 2021 21:32:07 +0100 Subject: isJobRunning() -> isJobPending() To be very clear what this function checks. See also #437. --- lib/room.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a9b2ba30..5e71881a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -858,7 +858,7 @@ const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. - if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) + if (q->joinedCount() <= membersMap.size() || isJobPending(allMembersJob)) return; allMembersJob = connection->callApi( @@ -1685,7 +1685,7 @@ QString Room::retryMessage(const QString& txnId) << "File for transaction" << txnId << "has already been uploaded, bypassing re-upload"; } else { - if (isJobRunning(transferIt->job)) { + if (isJobPending(transferIt->job)) { qCDebug(MESSAGES) << "Abandoning the upload job for transaction" << txnId << "and starting again"; transferIt->job->abandon(); @@ -1718,7 +1718,7 @@ void Room::discardMessage(const QString& txnId) const auto& transferIt = d->fileTransfers.find(txnId); if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); - if (isJobRunning(transferIt->job)) { + if (isJobPending(transferIt->job)) { transferIt->status = FileTransferInfo::Cancelled; transferIt->job->abandon(); emit fileTransferFailed(txnId, tr("File upload cancelled")); @@ -1924,7 +1924,7 @@ void Room::getPreviousContent(int limit, const QString &filter) { d->getPrevious void Room::Private::getPreviousContent(int limit, const QString &filter) { - if (isJobRunning(eventsHistoryJob)) + if (isJobPending(eventsHistoryJob)) return; eventsHistoryJob = @@ -1977,7 +1977,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); auto job = connection()->uploadFile(fileName, overrideContentType); - if (isJobRunning(job)) { + if (isJobPending(job)) { d->fileTransfers[id] = { job, fileName, true }; connect(job, &BaseJob::uploadProgress, this, [this, id](qint64 sent, qint64 total) { @@ -2033,7 +2033,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) qDebug(MAIN) << "File path:" << filePath; } auto job = connection()->downloadFile(fileUrl, filePath); - if (isJobRunning(job)) { + if (isJobPending(job)) { // If there was a previous transfer (completed or failed), overwrite it. d->fileTransfers[eventId] = { job, job->targetFileName() }; connect(job, &BaseJob::downloadProgress, this, @@ -2061,7 +2061,7 @@ void Room::cancelFileTransfer(const QString& id) << d->id; return; } - if (isJobRunning(it->job)) + if (isJobPending(it->job)) it->job->abandon(); d->fileTransfers.remove(id); emit fileTransferCancelled(id); -- cgit v1.2.3 From 0a775d9b3209be15dea8b8915fc0a1c8e0046ba6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 16 Jan 2021 18:19:45 +0100 Subject: Updated copyright statements upon Git audit After going through all the files and the history of commits on them it was clear that some copyright statements are obsolete (the code has been overwritten since) and some are missing. This commit tries best to remedy that, along with adding SPDX tags where they were still not used. Also, a minimal SPDX convention is documented for further contributions. Closes #426. --- lib/room.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index bc89464d..2e8641aa 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1,8 +1,11 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-FileCopyrightText: 2017 Roman Plášil +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-FileCopyrightText: 2018 Black Hat +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-FileCopyrightText: 2020 Ram Nad +// SPDX-License-Identifier: LGPL-2.1-or-later #include "room.h" -- cgit v1.2.3 From cd71f81a964751cc820074bb345f904b22a2c583 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 21:49:52 +0100 Subject: Room::P::removeMemberFromMap: comment that Q_ASSERT Maybe it's not even that bad, given that an effort is taken to recover from the internal member list corruption. --- lib/room.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 2e8641aa..fadcea17 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1404,8 +1404,9 @@ void Room::Private::removeMemberFromMap(User* u) if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(MEMBERS) << "...done in" << et; if (it != membersMap.cend()) { - Q_ASSERT_X(false, __FUNCTION__, - "Mismatched name in the room members list"); + // The assert (still) does more harm than good, it seems +// Q_ASSERT_X(false, __FUNCTION__, +// "Mismatched name in the room members list"); qCCritical(MEMBERS) << "Mismatched name in the room members list;" " avoiding the list corruption"; membersMap.remove(it.key(), u); -- cgit v1.2.3 From a95d5e83b4d0ab3a9bf2adef1ae4246adc317d04 Mon Sep 17 00:00:00 2001 From: Arnav Rawat Date: Thu, 11 Mar 2021 16:09:55 -0600 Subject: Support for pinned messages Fixes issue #188 --- lib/room.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index fadcea17..4f7f7ca9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -557,6 +557,16 @@ QString Room::canonicalAlias() const QString Room::displayName() const { return d->displayname; } +QList Room::pinnedEvents() const +{ + QStringList events = d->getCurrentState()->pinnedEvents(); + QList pinnedEvents; + QStringList::iterator i; + for (i = events.begin(); i != events.end(); ++i) + pinnedEvents.append(findInTimeline(*i)->event()); + return pinnedEvents; +} + void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const @@ -1830,6 +1840,10 @@ void Room::setCanonicalAlias(const QString& newAlias) d->requestSetState(newAlias, altAliases()); } +void Room::setPinnedMessages(const QStringList& events) +{ + d->requestSetState(events); +} void Room::setLocalAliases(const QStringList& aliases) { d->requestSetState(canonicalAlias(), aliases); -- cgit v1.2.3 From 5155f825bb71fca649a97ac4a7fa356cf85ee722 Mon Sep 17 00:00:00 2001 From: Arnav Rawat Date: Thu, 11 Mar 2021 18:33:54 -0600 Subject: historyedge() a result of findInTimeline() --- lib/room.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4f7f7ca9..ed07868b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -562,8 +562,11 @@ QList Room::pinnedEvents() const QStringList events = d->getCurrentState()->pinnedEvents(); QList pinnedEvents; QStringList::iterator i; - for (i = events.begin(); i != events.end(); ++i) - pinnedEvents.append(findInTimeline(*i)->event()); + for (i = events.begin(); i != events.end(); ++i) { + auto timelineItem = findInTimeline(*i); + if (timelineItem != historyEdge()) + pinnedEvents.append(timelineItem->event()); + } return pinnedEvents; } -- cgit v1.2.3 From 17bf4d180297c7e87363e179b8afa79ddb15dca7 Mon Sep 17 00:00:00 2001 From: Arnav Rawat Date: Tue, 25 May 2021 14:01:51 -0500 Subject: Fixes --- lib/room.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ed07868b..2a9cc0d8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -557,13 +557,17 @@ QString Room::canonicalAlias() const QString Room::displayName() const { return d->displayname; } -QList Room::pinnedEvents() const +QStringList Room::pinnedEventIds() const { + return d->getCurrentState()->pinnedEvents(); +} + +QVector< const Quotient::RoomEvent* > Quotient::Room::pinnedEvents() const { QStringList events = d->getCurrentState()->pinnedEvents(); - QList pinnedEvents; + QVector pinnedEvents; QStringList::iterator i; for (i = events.begin(); i != events.end(); ++i) { - auto timelineItem = findInTimeline(*i); + auto timelineItem = findInTimeline(*i); if (timelineItem != historyEdge()) pinnedEvents.append(timelineItem->event()); } @@ -1843,7 +1847,7 @@ void Room::setCanonicalAlias(const QString& newAlias) d->requestSetState(newAlias, altAliases()); } -void Room::setPinnedMessages(const QStringList& events) +void Room::setPinnedEvents(const QStringList& events) { d->requestSetState(events); } @@ -2610,6 +2614,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return AliasesChange; // clang-format off } + , [this] (const RoomPinnedEvent&) { + emit pinnedEventsChanged(); + return OtherChange; + } , [] (const RoomTopicEvent&) { return TopicChange; } -- cgit v1.2.3 From c18a591b484db451eb084ec4f1f17057813800df Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 20:51:17 +0200 Subject: Adjust to new moc/QMetaType requirements See https://www.qt.io/blog/whats-new-in-qmetatype-qvariant#qmetatype-knows-your-properties-and-methods-types --- lib/room.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index fadcea17..abaf50b7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -16,6 +16,9 @@ #include "syncdata.h" #include "user.h" +// NB: since Qt 6, moc_room.cpp needs User fully defined +#include "moc_room.cpp" + #include "csapi/account-data.h" #include "csapi/banning.h" #include "csapi/inviting.h" -- cgit v1.2.3 From 84d6295f859ee600d7aa3860767030bdc78914ba Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 20:57:54 +0200 Subject: uri.cpp, room.*: Get rid of QStringRefs --- lib/room.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index abaf50b7..c314fc72 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2962,12 +2962,16 @@ bool MemberSorter::operator()(User* u1, User* u2) const return operator()(u1, room->disambiguatedMemberName(u2->id())); } -bool MemberSorter::operator()(User* u1, const QString& u2name) const +bool MemberSorter::operator()(User* u1, QStringView u2name) const { auto n1 = room->disambiguatedMemberName(u1->id()); if (n1.startsWith('@')) n1.remove(0, 1); - auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); + const auto n2 = u2name.mid(u2name.startsWith('@') ? 1 : 0) +#if QT_VERSION_MAJOR < 6 + .toString() // Qt 5 doesn't have QStringView::localeAwareCompare +#endif + ; return n1.localeAwareCompare(n2) < 0; } -- cgit v1.2.3 From adb6725c04918d1323b4fc4e3a8cba91ae1dde60 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 10 Jul 2021 18:38:55 +0200 Subject: Fix Room::processAccountDataEvent() return value (cherry picked from commit 7b65051e959968fe538f40c975d85757cfcc7df7) (cherry picked from commit 9edfefe9b209583d18ce92e7ffd73e8aa1f3ef1e) --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c314fc72..0984bd96 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2766,9 +2766,9 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(STATE) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); - return Change::AccountDataChange; + changes |= Change::AccountDataChange; } - return Change::NoChange; + return changes; } template -- cgit v1.2.3 From 004ebf8d5ba095ca1b11e30d86cedc2ff8c0cfe7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 18:22:28 +0200 Subject: Room::postFile(): adjust to the changed RoomMessageEvent API 9a5fa623 dropped one of RoomMessageEvent constructors for Qt 6 in order to address #483 - breaking the build with Qt 6 along the way, as Room::postFile() relied on that constructor. This commit changes Room::postFile() in turn, deprecating the current signature and adding a new one that accepts an EventContent object rather than a path to a file. In order to achieve that, FileInfo and ImageInfo classes have gained new constructors that accept QFileInfo instead of the legacy series of parameters, streamlining usage of EventContent structures. --- lib/room.cpp | 105 ++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 40 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 0984bd96..6b729b8f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -306,6 +306,8 @@ public: return sendEvent(makeEvent(std::forward(eventArgs)...)); } + QString doPostFile(RoomEventPtr &&msgEvent, const QUrl &localUrl); + RoomEvent* addAsPending(RoomEventPtr&& event); QString doSendEvent(const RoomEvent* pEvent); @@ -1756,57 +1758,80 @@ QString Room::postReaction(const QString& eventId, const QString& key) return d->sendEvent(EventRelation::annotate(eventId, key)); } -QString Room::postFile(const QString& plainText, const QUrl& localPath, - bool asGenericFile) +QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) { - QFileInfo localFile { localPath.toLocalFile() }; - Q_ASSERT(localFile.isFile()); - - const auto txnId = - d->addAsPending( - makeEvent(plainText, localFile, asGenericFile)) - ->transactionId(); + const auto txnId = addAsPending(move(msgEvent))->transactionId(); // Remote URL will only be known after upload; fill in the local path // to enable the preview while the event is pending. - uploadFile(txnId, localPath); + q->uploadFile(txnId, localUrl); // Below, the upload job is used as a context object to clean up connections - const auto& transferJob = d->fileTransfers.value(txnId).job; - connect(this, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) { - if (id == txnId) { - auto it = findPendingEvent(txnId); - if (it != d->unsyncedEvents.end()) { - it->setFileUploaded(mxcUri); - emit pendingEventChanged( - int(it - d->unsyncedEvents.begin())); - d->doSendEvent(it->get()); - } else { - // Normally in this situation we should instruct - // the media server to delete the file; alas, there's no - // API specced for that. - qCWarning(MAIN) << "File uploaded to" << mxcUri - << "but the event referring to it was " - "cancelled"; - } + const auto& transferJob = fileTransfers.value(txnId).job; + connect(q, &Room::fileTransferCompleted, transferJob, + [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(mxcUri); + emit q->pendingEventChanged( + int(it - unsyncedEvents.begin())); + doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was " + "cancelled"; } }); - connect(this, &Room::fileTransferCancelled, transferJob, - [this, txnId](const QString& id) { - if (id == txnId) { - auto it = findPendingEvent(txnId); - if (it != d->unsyncedEvents.end()) { - const auto idx = int(it - d->unsyncedEvents.begin()); - emit pendingEventAboutToDiscard(idx); - // See #286 on why iterator may not be valid here. - d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); - emit pendingEventDiscarded(); - } - } + connect(q, &Room::fileTransferCancelled, transferJob, + [this, txnId](const QString& tId) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) + return; + + const auto idx = int(it - unsyncedEvents.begin()); + emit q->pendingEventAboutToDiscard(idx); + // See #286 on why `it` may not be valid here. + unsyncedEvents.erase(unsyncedEvents.begin() + idx); + emit q->pendingEventDiscarded(); }); return txnId; } +QString Room::postFile(const QString& plainText, + EventContent::TypedBase* content) +{ + Q_ASSERT(content != nullptr && content->fileInfo() != nullptr); + const auto* const fileInfo = content->fileInfo(); + Q_ASSERT(fileInfo != nullptr); + QFileInfo localFile { fileInfo->url.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + + return d->doPostFile( + makeEvent( + plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content), + fileInfo->url); +} + +#if QT_VERSION_MAJOR < 6 +QString Room::postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile) +{ + QFileInfo localFile { localPath.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + return d->doPostFile(makeEvent(plainText, localFile, + asGenericFile), + localPath); +} +#endif + QString Room::postEvent(RoomEvent* event) { return d->sendEvent(RoomEventPtr(event)); -- cgit v1.2.3 From c05b5c2b79f9ab301fee587ee781b9c8e18b8a2f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 16 Jul 2021 20:03:06 +0200 Subject: MembershipType -> Membership, also used for JoinState Instead of being defined independently, JoinState now uses values from the Membership enumeration (former MemberEventContent::MembershipType) that was moved to quotient_common.h for that purpose. Both enumerations gained a Q_FLAG_NS decoration and operator<< overrides that strip "Quotient::" prefix when dumping member/join state values to the log - obviating toCString(JoinState) along the way. Quotient::MembershipType alias is deprecated from now. --- lib/room.cpp | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6b729b8f..54c67c2f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -460,7 +460,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); - qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id; + qCDebug(STATE) << "New" << initialJoinState << "Room:" << id; } Room::~Room() { delete d; } @@ -603,6 +603,11 @@ JoinState Room::memberJoinState(User* user) const : JoinState::Leave; } +Membership Room::memberState(User* user) const +{ + return d->getCurrentState(user->id())->membership(); +} + JoinState Room::joinState() const { return d->joinState; } void Room::setJoinState(JoinState state) @@ -611,8 +616,8 @@ void Room::setJoinState(JoinState state) if (state == oldState) return; d->joinState = state; - qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState) - << "->" << int(state); + qCDebug(STATE) << "Room" << id() << "changed state: " << oldState + << "->" << state; emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } @@ -2513,16 +2518,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return false; // Stay low and hope for the best... } const auto prevMembership = oldRme ? oldRme->membership() - : MembershipType::Leave; + : Membership::Leave; switch (prevMembership) { - case MembershipType::Invite: + case Membership::Invite: if (rme.membership() != prevMembership) { d->usersInvited.removeOne(u); Q_ASSERT(!d->usersInvited.contains(u)); } break; - case MembershipType::Join: - if (rme.membership() == MembershipType::Join) { + case Membership::Join: + if (rme.membership() == Membership::Join) { // rename/avatar change or no-op if (rme.newDisplayName()) { emit memberAboutToRename(u, *rme.newDisplayName()); @@ -2536,7 +2541,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return false; } } else { - if (rme.membership() == MembershipType::Invite) + if (rme.membership() == Membership::Invite) qCWarning(MAIN) << "Membership change from Join to Invite:" << rme; // whatever the new membership, it's no more Join @@ -2544,16 +2549,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit userRemoved(u); } break; - case MembershipType::Ban: - case MembershipType::Knock: - case MembershipType::Leave: - if (rme.membership() == MembershipType::Invite - || rme.membership() == MembershipType::Join) { + case Membership::Ban: + case Membership::Knock: + case Membership::Leave: + if (rme.membership() == Membership::Invite + || rme.membership() == Membership::Join) { d->membersLeft.removeOne(u); Q_ASSERT(!d->membersLeft.contains(u)); } break; - case MembershipType::Undefined: + case Membership::Undefined: ; // A warning will be dropped in the post-processing block below } return true; @@ -2636,10 +2641,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) static_cast(oldStateEvent); const auto prevMembership = oldMemberEvent ? oldMemberEvent->membership() - : MembershipType::Leave; + : Membership::Leave; switch (evt.membership()) { - case MembershipType::Join: - if (prevMembership != MembershipType::Join) { + case Membership::Join: + if (prevMembership != Membership::Join) { d->insertMemberIntoMap(u); emit userAdded(u); } else { @@ -2651,19 +2656,19 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit memberAvatarChanged(u); } break; - case MembershipType::Invite: + case Membership::Invite: if (!d->usersInvited.contains(u)) d->usersInvited.push_back(u); if (u == localUser() && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); break; - case MembershipType::Knock: - case MembershipType::Ban: - case MembershipType::Leave: + case Membership::Knock: + case Membership::Ban: + case Membership::Leave: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); break; - case MembershipType::Undefined: + case Membership::Undefined: qCWarning(MEMBERS) << "Ignored undefined membership type"; } return MembersChange; -- cgit v1.2.3 From cddf3c6a2ab7481e5816ca7632b9f919efa0ac40 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 24 Jul 2021 20:56:50 +0200 Subject: Wrap SyncRoomData counters into Omittables Also: introduce a merge(T1&, const Omittable&) that does pretty much the same as Omittable::merge(const Omittable&) except it works on non-omittables as the left/first operand. The change removes the need for a clumsy -2 fallback in unreadCount, and makes the logic loading those counters cleaner along the way. --- lib/room.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 54c67c2f..076fd8c8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1570,20 +1570,18 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { - qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; - d->unreadMessages = data.unreadCount; + if (merge(d->unreadMessages, data.unreadCount)) { + qCDebug(MESSAGES) << "Loaded unread_count:" << *data.unreadCount // + << "in" << objectName(); emit unreadMessagesChanged(this); } - if (data.highlightCount != d->highlightCount) { - d->highlightCount = data.highlightCount; + if (merge(d->highlightCount, data.highlightCount)) emit highlightCountChanged(); - } - if (data.notificationCount != d->notificationCount) { - d->notificationCount = data.notificationCount; + + if (merge(d->notificationCount, data.notificationCount)) emit notificationCountChanged(); - } + if (roomChanges != Change::NoChange) { d->updateDisplayname(); emit changed(roomChanges); -- cgit v1.2.3 From 4ef72d95868451b7e53bd49a873648332a121130 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 28 Jul 2021 03:33:24 +0200 Subject: setFirst/LastDisplayedEvent(): warn about unloaded events --- lib/room.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 076fd8c8..10e827d7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -909,6 +909,11 @@ void Room::setFirstDisplayedEventId(const QString& eventId) if (d->firstDisplayedEventId == eventId) return; + if (!eventId.isEmpty() && findInTimeline(eventId) == historyEdge()) + qCWarning(MESSAGES) + << eventId + << "is marked as first displayed but doesn't seem to be loaded"; + d->firstDisplayedEventId = eventId; emit firstDisplayedEventChanged(); } @@ -931,6 +936,12 @@ void Room::setLastDisplayedEventId(const QString& eventId) if (d->lastDisplayedEventId == eventId) return; + const auto marker = findInTimeline(eventId); + if (!eventId.isEmpty() && marker == historyEdge()) + qCWarning(MESSAGES) + << eventId + << "is marked as last displayed but doesn't seem to be loaded"; + d->lastDisplayedEventId = eventId; emit lastDisplayedEventChanged(); } -- cgit v1.2.3 From eed5a6f127ec3bb1553ac629457f196d8893665a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 26 Jul 2021 08:19:29 +0200 Subject: Room::usersAtEventId(): switch from QMultiHash to QHash of QSets While slightly more complex for updating, this allows COW to kick in in the read accessor; using QSet instead of QList also provides better consistency guarantees. For QML both are converted to an Array-like collection since Qt 5.15; Qt 5.12 turns QSet<> in a QVariantList, according to the documentation, which is quite reasonable too. --- lib/room.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 10e827d7..13589ee9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -121,7 +121,7 @@ public: int notificationCount = 0; members_map_t membersMap; QList usersTyping; - QMultiHash eventIdReadUsers; + QHash> eventIdReadUsers; QList usersInvited; QList membersLeft; int unreadMessages = 0; @@ -627,8 +627,11 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) auto& storedId = lastReadEventIds[u]; if (storedId == eventId) return Change::NoChange; - eventIdReadUsers.remove(storedId, u); - eventIdReadUsers.insert(eventId, u); + auto& oldEventReadUsers = eventIdReadUsers[storedId]; + oldEventReadUsers.remove(u); + if (oldEventReadUsers.isEmpty()) + eventIdReadUsers.remove(storedId); + eventIdReadUsers[eventId].insert(u); swap(storedId, eventId); emit q->lastReadEventChanged(u); emit q->readMarkerForUserMoved(u, eventId, storedId); @@ -965,9 +968,9 @@ QString Room::readMarkerEventId() const return d->lastReadEventIds.value(localUser()); } -QList Room::usersAtEventId(const QString& eventId) +QSet Room::usersAtEventId(const QString& eventId) { - return d->eventIdReadUsers.values(eventId); + return d->eventIdReadUsers.value(eventId); } int Room::notificationCount() const { return d->notificationCount; } -- cgit v1.2.3 From 277f43defe3fa55ff32fe53952c6331a81d65a20 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 28 Jul 2021 03:32:02 +0200 Subject: Room: fix the read markers/receipts confusion This turns the design changes laid out in #464 comments to code, as of 0.6.x branch (0.7 API will be introduced separately): - readMarker() now returns the fully read marker, unlike readMarker(User*) that returns a read receipt, even when called for the local user. - Private::setLastReadEvent() -> setLastReadReceipt(), incorporating the "promotion" logic from promoteReadReceipt(). - The above makes promoteReadReceipt() unneeded; the remaining piece of logic that recalculates the number of unread messages is put to its own method - Private::recalculateUnreadCount(). - Private::updateUnreadCount() is only slightly refreshed, continues to use the fully read marker position (as it used to). - Now that read receipts and fully read markers are managed separately, Private::setLastReadReceipt() has got its counterpart, Private::setFullyReadMarker(); both only update their respective markers locally (emitting signals as needed), without interaction with the homeserver. - Private::markMessagesAsRead() now delegates updating the fully read marker to setFullyReadMarker() and on top of that sends the new fully read marker to the homeserver. - Private::serverReadMarker -> fullyReadUntilEventId (to be really clear what it stores). - The hand-written PostReadMarkersJob is replaced with the generated SetReadMarkerJob that does the same thing (and can update the read receipt on top). --- lib/room.cpp | 341 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 180 insertions(+), 161 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 13589ee9..ec3a9532 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -129,7 +129,7 @@ public: QString firstDisplayedEventId; QString lastDisplayedEventId; QHash lastReadEventIds; - QString serverReadMarker; + QString fullyReadUntilEventId; TagsMap tags; UnorderedMap accountData; QString prevBatch; @@ -290,11 +290,12 @@ public: */ void dropDuplicateEvents(RoomEvents& events) const; - Changes setLastReadEvent(User* u, QString eventId); - void updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); - Changes promoteReadMarker(User* u, const rev_iter_t& newMarker, bool force = false); - - Changes markMessagesAsRead(rev_iter_t upToMarker); + void setLastReadReceipt(User* u, rev_iter_t newMarker, + QString newEvtId = {}); + Changes setFullyReadMarker(const QString &eventId); + Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); + Changes recalculateUnreadCount(bool force = false); + void markMessagesAsRead(const rev_iter_t &upToMarker); void getAllMembers(); @@ -622,142 +623,172 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) +void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, + QString newEvtId) { + if (!u) { + Q_ASSERT(u != nullptr); // For Debug builds + qCCritical(MAIN) << "Empty user, skipping read receipt registration"; + return; // For Release builds + } + if (q->memberJoinState(u) != JoinState::Join) { + qCWarning(MAIN) << "Won't record read receipt for non-member" << u->id(); + return; + } + + if (newMarker == timeline.crend() && !newEvtId.isEmpty()) + newMarker = q->findInTimeline(newEvtId); + if (newMarker != timeline.crend()) { + // NB: with reverse iterators, timeline history >= sync edge + if (newMarker >= q->readMarker(u)) + return; + + // Try to auto-promote the read marker over the user's own messages + // (switch to direct iterators for that). + const auto eagerMarker = find_if(newMarker.base(), timeline.cend(), + [=](const TimelineItem& ti) { + return ti->senderId() != u->id(); + }) + - 1; + newEvtId = (*eagerMarker)->id(); + if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt + qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id() + << "to" << newEvtId; + } + auto& storedId = lastReadEventIds[u]; - if (storedId == eventId) - return Change::NoChange; + if (storedId == newEvtId) + return; + // Finally make the change auto& oldEventReadUsers = eventIdReadUsers[storedId]; oldEventReadUsers.remove(u); if (oldEventReadUsers.isEmpty()) eventIdReadUsers.remove(storedId); - eventIdReadUsers[eventId].insert(u); - swap(storedId, eventId); + eventIdReadUsers[newEvtId].insert(u); + swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId emit q->lastReadEventChanged(u); - emit q->readMarkerForUserMoved(u, eventId, storedId); - if (isLocalUser(u)) { - if (storedId != serverReadMarker) - connection->callApi(BackgroundRequest, id, - storedId); - emit q->readMarkerMoved(eventId, storedId); - return Change::ReadMarkerChange; - } - return Change::NoChange; + if (!isLocalUser(u)) + emit q->readMarkerForUserMoved(u, newEvtId, storedId); } -void Room::Private::updateUnreadCount(const rev_iter_t& from, - const rev_iter_t& to) +Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, + const rev_iter_t& to) { Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); - // Catch a special case when the last read event id refers to an event - // that has just arrived. In this case we should recalculate - // unreadMessages and might need to promote the read marker further - // over local-origin messages. - auto readMarker = q->readMarker(); - if (readMarker == timeline.crend() && q->allHistoryLoaded()) - --readMarker; // Read marker not found in the timeline, initialise it - if (readMarker >= from && readMarker < to) { - promoteReadMarker(q->localUser(), readMarker, true); - return; - } - - Q_ASSERT(to <= readMarker); + auto fullyReadMarker = q->readMarker(); + if (fullyReadMarker < from) + return NoChange; // What's arrived is already fully read + + if (fullyReadMarker == timeline.crend() && q->allHistoryLoaded()) + --fullyReadMarker; // No read marker in the whole room, initialise it + if (fullyReadMarker < to) { + // Catch a special case when the last fully read event id refers to an + // event that has just arrived. In this case we should recalculate + // unreadMessages to get an exact number instead of an estimation + // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). + // For the same reason (switching from the estimation to the exact + // number) this branch always emits unreadMessagesChanged() and returns + // UnreadNotifsChange, even if the estimation luckily matched the exact + // result. + return recalculateUnreadCount(true); + } + + // Fully read marker is somewhere beyond the most historical message from + // the arrived batch - add up newly arrived messages to the current counter, + // instead of a complete recalculation. + Q_ASSERT(to <= fullyReadMarker); QElapsedTimer et; et.start(); const auto newUnreadMessages = - count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); + count_if(from, to, + std::bind(&Room::Private::isEventNotable, this, _1)); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages took" << et; - if (newUnreadMessages > 0) { - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages < 0) - unreadMessages = 0; - - unreadMessages += newUnreadMessages; - qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() - ? "in total at least" - : "in total") - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - } -} + if (newUnreadMessages == 0) + return NoChange; -Room::Changes Room::Private::promoteReadMarker(User* u, - const rev_iter_t& newMarker, - bool force) -{ - Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); - Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); + // See https://github.com/quotient-im/libQuotient/wiki/unread_count + if (unreadMessages < 0) + unreadMessages = 0; + + unreadMessages += newUnreadMessages; + qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" + << newUnreadMessages << "unread message(s)," + << (q->readMarker() == timeline.crend() + ? "in total at least" + : "in total") + << unreadMessages << "unread message(s)"; + emit q->unreadMessagesChanged(q); + return UnreadNotifsChange; +} + +Room::Changes Room::Private::recalculateUnreadCount(bool force) +{ + // The recalculation logic assumes that the fully read marker points at + // a specific position in the timeline + Q_ASSERT(q->readMarker() != timeline.crend()); + const auto oldUnreadCount = unreadMessages; + QElapsedTimer et; + et.start(); + unreadMessages = + int(count_if(timeline.crbegin(), q->readMarker(), + [this](const auto& ti) { return isEventNotable(ti); })); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(PROFILER) << "Recounting unread messages took" << et; - const auto prevMarker = q->readMarker(u); - if (!force && prevMarker <= newMarker) // Remember, we deal with reverse - // iterators - return Change::NoChange; + // See https://github.com/quotient-im/libQuotient/wiki/unread_count + if (unreadMessages == 0) + unreadMessages = -1; - Q_ASSERT(newMarker < timeline.crend()); + if (!force && unreadMessages == oldUnreadCount) + return NoChange; - // Try to auto-promote the read marker over the user's own messages - // (switch to direct iterators for that). - auto eagerMarker = - find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { - return ti->senderId() != u->id(); - }); + if (unreadMessages == -1) + qCDebug(MESSAGES) + << "Room" << displayname << "has no more unread messages"; + else + qCDebug(MESSAGES) << "Room" << displayname << "still has" + << unreadMessages << "unread message(s)"; + emit q->unreadMessagesChanged(q); + return UnreadNotifsChange; +} - auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); - if (isLocalUser(u)) { - const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; - et.start(); - unreadMessages = - int(count_if(eagerMarker, timeline.cend(), - [this](const auto& ti) { return isEventNotable(ti); })); - if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Recounting unread messages took" << et; +Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) +{ + if (fullyReadUntilEventId == eventId) + return NoChange; - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; + const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); + qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // + << "moved to" << fullyReadUntilEventId; + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - if (force || unreadMessages != oldUnreadCount) { - if (unreadMessages == -1) { - qCDebug(MESSAGES) - << "Room" << displayname << "has no more unread messages"; - } else - qCDebug(MESSAGES) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - changes |= Change::UnreadNotifsChange; - } + Changes changes = ReadMarkerChange; + if (const auto rm = q->readMarker(); rm != timeline.crend()) { + // Pull read receipt if it's behind + if (auto rr = q->readMarker(q->localUser()); rr > rm) + setLastReadReceipt(q->localUser(), rm); + + changes |= recalculateUnreadCount(); } return changes; } -Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - const auto prevMarker = q->readMarker(); - auto changes = promoteReadMarker(q->localUser(), upToMarker); - if (prevMarker != upToMarker) - qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker(); - - // We shouldn't send read receipts for the local user's own messages - so - // search earlier messages for the latest message not from the local user - // until the previous last-read message, whichever comes first. - for (; upToMarker < prevMarker; ++upToMarker) { - if ((*upToMarker)->senderId() != q->localUser()->id()) { - connection->callApi(BackgroundRequest, - id, QStringLiteral("m.read"), - QUrl::toPercentEncoding( - (*upToMarker)->id())); - break; - } + if (upToMarker < q->readMarker()) { + setFullyReadMarker((*upToMarker)->id()); + // Assuming that if a read receipt was sent on a newer event, it will + // stay there instead of "un-reading" notifications/mentions from + // m.fully_read to m.read + connection->callApi(BackgroundRequest, id, + fullyReadUntilEventId, + fullyReadUntilEventId); } - return changes; } void Room::markMessagesAsRead(QString uptoEventId) @@ -961,11 +992,14 @@ Room::rev_iter_t Room::readMarker(const User* user) const return findInTimeline(d->lastReadEventIds.value(user)); } -Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); } +Room::rev_iter_t Room::readMarker() const +{ + return findInTimeline(d->fullyReadUntilEventId); +} QString Room::readMarkerEventId() const { - return d->lastReadEventIds.value(localUser()); + return d->fullyReadUntilEventId; } QSet Room::usersAtEventId(const QString& eventId) @@ -2436,24 +2470,21 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << timeline.back(); // The first event in the just-added batch (referred to by `from`) - // defines whose read marker can possibly be promoted any further over + // defines whose read receipt can possibly be promoted any further over // the same author's events newly arrived. Others will need explicit - // read receipts from the server (or, for the local user, - // markMessagesAsRead() invocation) to promote their read markers over + // read receipts from the server - or, for the local user, calling + // setLastDisplayedEventId() - to promote their read receipts over // the new message events. - if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) { - auto* const firstWriter = q->user(senderId); - if (q->readMarker(firstWriter) != timeline.crend()) { - roomChanges |= - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(MESSAGES) - << "Auto-promoted read marker for" << senderId - << "to" << *q->readMarker(firstWriter); - } + auto* const firstWriter = q->user((*from)->senderId()); + setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); + if (firstWriter == q->localUser() && q->readMarker().base() == from) { + // If the local user's message(s) is/are first in the batch + // and the fully read marker was right before it, promote + // the fully read marker to the same event as the read receipt. + roomChanges |= + setFullyReadMarker(lastReadEventIds.value(firstWriter)); } - - updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); - roomChanges |= Change::UnreadNotifsChange; + roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } Q_ASSERT(timeline.size() == timelineSize + totalInserted); @@ -2498,8 +2529,11 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) emit q->updatedEvent(relation.eventId); } } - if (from <= q->readMarker()) - updateUnreadCount(from, timeline.crend()); + updateUnreadCount(from, timeline.crend()); + // When there are no unread messages and the read marker is within the + // known timeline, unreadMessages == -1 + // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). + Q_ASSERT(unreadMessages != 0 || q->readMarker() == timeline.crend()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) @@ -2723,7 +2757,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) if (auto* evt = eventCast(event)) { d->usersTyping.clear(); for (const auto& userId : evt->users()) { - auto u = user(userId); + auto* const u = user(userId); if (memberJoinState(u) == JoinState::Join) d->usersTyping.append(u); } @@ -2746,30 +2780,21 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) << p.receipts.size() << "users"; } const auto newMarker = findInTimeline(p.evtId); - if (newMarker != timelineEdge()) { - for (const Receipt& r : p.receipts) { - if (r.userId == connection()->userId()) - continue; // FIXME, #185 - auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join) - changes |= d->promoteReadMarker(u, newMarker); - } - } else { - qCDebug(EPHEMERAL) << "Event" << p.evtId - << "not found; saving read receipts anyway"; - // If the event is not found (most likely, because it's too old - // and hasn't been fetched from the server yet), but there is - // a previous marker for a user, keep the previous marker. - // Otherwise, blindly store the event id for this user. - for (const Receipt& r : p.receipts) { - if (r.userId == connection()->userId()) - continue; // FIXME, #185 - auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join - && readMarker(u) == timelineEdge()) - changes |= d->setLastReadEvent(u, p.evtId); + if (newMarker == historyEdge()) + qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " + "found; saving them anyway"; + for (const Receipt& r : p.receipts) + if (auto* const u = user(r.userId); + memberJoinState(u) == JoinState::Join) { + // If the event is not found (most likely, because it's + // too old and hasn't been fetched from the server yet) + // but there is a previous marker for a user, keep + // the previous marker because read receipts are not + // supposed to move backwards. Otherwise, blindly + // store the event id for this user and update the read + // marker when/if the event is fetched later on. + d->setLastReadReceipt(u, newMarker, p.evtId); } - } } if (eventsWithReceipts.size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) @@ -2789,15 +2814,9 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) changes |= Change::TagsChange; } - if (auto* evt = eventCast(event)) { - auto readEventId = evt->event_id(); - qCDebug(STATE) << "Server-side read marker at" << readEventId; - d->serverReadMarker = readEventId; - const auto newMarker = findInTimeline(readEventId); - changes |= newMarker != timelineEdge() - ? d->markMessagesAsRead(newMarker) - : d->setLastReadEvent(localUser(), readEventId); - } + if (auto* evt = eventCast(event)) + changes |= d->setFullyReadMarker(evt->event_id()); + // For all account data events auto& currentData = d->accountData[event->matrixType()]; // A polymorphic event-specific comparison might be a bit more -- cgit v1.2.3 From e0d9125de7ac132c2a54152015687abbe5e73193 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 30 Jul 2021 08:10:48 +0200 Subject: Room: drop 0.6 deprecations; deprecate RoomAliasEvent Namely memberCount(), localAliases(), remoteAliases(), timelineEdge(). --- lib/room.cpp | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 10e827d7..f223e0b9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -540,21 +540,6 @@ QStringList Room::altAliases() const return d->getCurrentState()->altAliases(); } -QStringList Room::localAliases() const -{ - return d->getCurrentState( - connection()->domain()) - ->aliases(); -} - -QStringList Room::remoteAliases() const -{ - QStringList result; - for (const auto& s : std::as_const(d->aliasServers)) - result += d->getCurrentState(s)->aliases(); - return result; -} - QString Room::canonicalAlias() const { return d->getCurrentState()->alias(); @@ -793,8 +778,6 @@ Room::Timeline::const_iterator Room::syncEdge() const return d->timeline.cend(); } -Room::rev_iter_t Room::timelineEdge() const { return historyEdge(); } - TimelineItem::index_t Room::minTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.front().index(); @@ -813,7 +796,7 @@ bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const { - return timelineEdge() + return historyEdge() - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); } @@ -1279,8 +1262,6 @@ QStringList Room::htmlSafeMemberNames() const return res; } -int Room::memberCount() const { return d->membersMap.size(); } - int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const @@ -2153,8 +2134,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, { QStringLiteral("ban"), QStringLiteral("events"), QStringLiteral("events_default"), QStringLiteral("kick"), QStringLiteral("redact"), QStringLiteral("state_default"), - QStringLiteral("users"), QStringLiteral("users_default") } }, - { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } + QStringLiteral("users"), QStringLiteral("users_default") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomHistoryVisibility::typeId(), // { QStringLiteral("history_visibility") } } @@ -2743,7 +2723,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) << p.receipts.size() << "users"; } const auto newMarker = findInTimeline(p.evtId); - if (newMarker != timelineEdge()) { + if (newMarker != historyEdge()) { for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 @@ -2763,7 +2743,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join - && readMarker(u) == timelineEdge()) + && readMarker(u) == historyEdge()) changes |= d->setLastReadEvent(u, p.evtId); } } @@ -2791,7 +2771,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(STATE) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); - changes |= newMarker != timelineEdge() + changes |= newMarker != historyEdge() ? d->markMessagesAsRead(newMarker) : d->setLastReadEvent(localUser(), readEventId); } -- cgit v1.2.3 From 02eac9e9ff573d85677f900288e82473f5296ee3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 30 Jul 2021 08:12:09 +0200 Subject: Use a better type in makeRedacted --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f223e0b9..47a0ec94 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2127,7 +2127,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, QStringLiteral("membership") }; // clang-format on - std::vector> keepContentKeysMap { + static const std::pair keepContentKeysMap[] { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }, { RoomPowerLevelsEvent::typeId(), @@ -2146,9 +2146,9 @@ RoomEventPtr makeRedacted(const RoomEvent& target, ++it; } auto keepContentKeys = - find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), + find_if(begin(keepContentKeysMap), end(keepContentKeysMap), [&target](const auto& t) { return target.type() == t.first; }); - if (keepContentKeys == keepContentKeysMap.end()) { + if (keepContentKeys == end(keepContentKeysMap)) { originalJson.remove(ContentKeyL); originalJson.remove(PrevContentKeyL); } else { -- cgit v1.2.3 From 62c829cc8de8a870c08926c41331f2766e766f37 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 29 Jul 2021 21:58:56 +0200 Subject: Quotient::ReadReceipt; deprecated readMarker() It's now possible to get receipts along with their timestamps by calling Room::lastReadReceipt(). Together this new method, fullyReadMarker(), and lastFullyReadEventId() deprecate readMarker() overloads and readMarkerEventId() respectively. lastFullyReadEventId is also a Q_PROPERTY (deprecating readMarkerEventId); readMarkerMoved() signal is deprecated by fullyReadMarkerMoved(), while readMarkerForUserMoved() is deprecated in favour of existing lastReadEventChanged(). --- lib/room.cpp | 79 ++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 45 insertions(+), 34 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ec3a9532..9a571127 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -128,7 +128,7 @@ public: bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; - QHash lastReadEventIds; + QHash lastReadReceipts; QString fullyReadUntilEventId; TagsMap tags; UnorderedMap accountData; @@ -291,7 +291,7 @@ public: void dropDuplicateEvents(RoomEvents& events) const; void setLastReadReceipt(User* u, rev_iter_t newMarker, - QString newEvtId = {}); + ReadReceipt newReceipt = {}); Changes setFullyReadMarker(const QString &eventId); Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); Changes recalculateUnreadCount(bool force = false); @@ -624,7 +624,7 @@ void Room::setJoinState(JoinState state) } void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, - QString newEvtId) + ReadReceipt newReceipt) { if (!u) { Q_ASSERT(u != nullptr); // For Debug builds @@ -636,11 +636,12 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, return; } - if (newMarker == timeline.crend() && !newEvtId.isEmpty()) - newMarker = q->findInTimeline(newEvtId); + auto& storedReceipt = lastReadReceipts[u]; + if (newMarker == timeline.crend() && !newReceipt.eventId.isEmpty()) + newMarker = q->findInTimeline(newReceipt.eventId); if (newMarker != timeline.crend()) { // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->readMarker(u)) + if (newMarker >= q->findInTimeline(storedReceipt.eventId)) return; // Try to auto-promote the read marker over the user's own messages @@ -650,25 +651,26 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, return ti->senderId() != u->id(); }) - 1; - newEvtId = (*eagerMarker)->id(); + newReceipt.eventId = (*eagerMarker)->id(); if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id() - << "to" << newEvtId; + << "to" << newReceipt.eventId; } - auto& storedId = lastReadEventIds[u]; - if (storedId == newEvtId) + if (storedReceipt == newReceipt) return; // Finally make the change - auto& oldEventReadUsers = eventIdReadUsers[storedId]; + auto& oldEventReadUsers = eventIdReadUsers[storedReceipt.eventId]; oldEventReadUsers.remove(u); if (oldEventReadUsers.isEmpty()) - eventIdReadUsers.remove(storedId); - eventIdReadUsers[newEvtId].insert(u); - swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId + eventIdReadUsers.remove(storedReceipt.eventId); + eventIdReadUsers[newReceipt.eventId].insert(u); + swap(storedReceipt, newReceipt); // Now newReceipt actually stores the old receipt emit q->lastReadEventChanged(u); + // TODO: remove in 0.8 if (!isLocalUser(u)) - emit q->readMarkerForUserMoved(u, newEvtId, storedId); + emit q->readMarkerForUserMoved(u, newReceipt.eventId, + storedReceipt.eventId); } Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, @@ -677,7 +679,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); - auto fullyReadMarker = q->readMarker(); + auto fullyReadMarker = q->fullyReadMarker(); if (fullyReadMarker < from) return NoChange; // What's arrived is already fully read @@ -718,7 +720,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, unreadMessages += newUnreadMessages; qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() + << (q->fullyReadMarker() == timeline.crend() ? "in total at least" : "in total") << unreadMessages << "unread message(s)"; @@ -730,12 +732,12 @@ Room::Changes Room::Private::recalculateUnreadCount(bool force) { // The recalculation logic assumes that the fully read marker points at // a specific position in the timeline - Q_ASSERT(q->readMarker() != timeline.crend()); + Q_ASSERT(q->fullyReadMarker() != timeline.crend()); const auto oldUnreadCount = unreadMessages; QElapsedTimer et; et.start(); unreadMessages = - int(count_if(timeline.crbegin(), q->readMarker(), + int(count_if(timeline.crbegin(), q->fullyReadMarker(), [this](const auto& ti) { return isEventNotable(ti); })); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; @@ -765,22 +767,22 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // << "moved to" << fullyReadUntilEventId; + emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); + // TODO: Remove in 0.8 emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); Changes changes = ReadMarkerChange; - if (const auto rm = q->readMarker(); rm != timeline.crend()) { + if (const auto rm = q->fullyReadMarker(); rm != timeline.crend()) { // Pull read receipt if it's behind - if (auto rr = q->readMarker(q->localUser()); rr > rm) - setLastReadReceipt(q->localUser(), rm); - - changes |= recalculateUnreadCount(); + setLastReadReceipt(q->localUser(), rm); + changes |= recalculateUnreadCount(); // TODO: updateUnreadCount()? } return changes; } void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - if (upToMarker < q->readMarker()) { + if (upToMarker < q->fullyReadMarker()) { setFullyReadMarker((*upToMarker)->id()); // Assuming that if a read receipt was sent on a newer event, it will // stay there instead of "un-reading" notifications/mentions from @@ -989,17 +991,23 @@ void Room::setLastDisplayedEvent(TimelineItem::index_t index) Room::rev_iter_t Room::readMarker(const User* user) const { Q_ASSERT(user); - return findInTimeline(d->lastReadEventIds.value(user)); + return findInTimeline(lastReadReceipt(user->id()).eventId); } -Room::rev_iter_t Room::readMarker() const +Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); } + +QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } + +ReadReceipt Room::lastReadReceipt(const QString& userId) const { - return findInTimeline(d->fullyReadUntilEventId); + return d->lastReadReceipts.value(user(userId)); } -QString Room::readMarkerEventId() const +QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } + +Room::rev_iter_t Room::fullyReadMarker() const { - return d->fullyReadUntilEventId; + return findInTimeline(d->fullyReadUntilEventId); } QSet Room::usersAtEventId(const QString& eventId) @@ -2477,12 +2485,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // the new message events. auto* const firstWriter = q->user((*from)->senderId()); setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); - if (firstWriter == q->localUser() && q->readMarker().base() == from) { + if (firstWriter == q->localUser() + && q->fullyReadMarker().base() == from) // + { // If the local user's message(s) is/are first in the batch // and the fully read marker was right before it, promote // the fully read marker to the same event as the read receipt. roomChanges |= - setFullyReadMarker(lastReadEventIds.value(firstWriter)); + setFullyReadMarker(lastReadReceipts.value(firstWriter).eventId); } roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } @@ -2533,7 +2543,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) // When there are no unread messages and the read marker is within the // known timeline, unreadMessages == -1 // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). - Q_ASSERT(unreadMessages != 0 || q->readMarker() == timeline.crend()); + Q_ASSERT(unreadMessages != 0 || q->fullyReadMarker() == timeline.crend()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) @@ -2793,7 +2803,8 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) // supposed to move backwards. Otherwise, blindly // store the event id for this user and update the read // marker when/if the event is fetched later on. - d->setLastReadReceipt(u, newMarker, p.evtId); + d->setLastReadReceipt(u, newMarker, + { p.evtId, r.timestamp }); } } if (eventsWithReceipts.size() > 3 || totalReceipts > 10 -- cgit v1.2.3 From 2975b435f1ae198187183b883b5e666c7659e8fc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 30 Jul 2021 23:25:01 +0200 Subject: Room::setReadReceipt() --- lib/room.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c7f29b6d..649b1ed2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -765,6 +765,15 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) return changes; } +void Room::setReadReceipt(const QString& atEventId) +{ + d->setLastReadReceipt(localUser(), historyEdge(), + { atEventId, QDateTime::currentDateTime() }); + connection()->callApi(BackgroundRequest, id(), + QStringLiteral("m.read"), + QUrl::toPercentEncoding(atEventId)); +} + void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { if (upToMarker < q->fullyReadMarker()) { -- cgit v1.2.3 From 8093ea85347588431c0ddbf6e178a4db5f51b909 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 1 Aug 2021 17:19:23 +0200 Subject: Enhanced logging for read receipts --- lib/room.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 649b1ed2..e7d4e137 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -617,7 +617,8 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, return; // For Release builds } if (q->memberJoinState(u) != JoinState::Join) { - qCWarning(MAIN) << "Won't record read receipt for non-member" << u->id(); + qCWarning(EPHEMERAL) + << "Won't record read receipt for non-member" << u->id(); return; } @@ -626,8 +627,11 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, newMarker = q->findInTimeline(newReceipt.eventId); if (newMarker != timeline.crend()) { // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->findInTimeline(storedReceipt.eventId)) + if (newMarker >= q->findInTimeline(storedReceipt.eventId)) { + qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() + << "is at or behind the old one, skipping"; return; + } // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). @@ -651,6 +655,8 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, eventIdReadUsers.remove(storedReceipt.eventId); eventIdReadUsers[newReceipt.eventId].insert(u); swap(storedReceipt, newReceipt); // Now newReceipt actually stores the old receipt + qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at" + << storedReceipt.eventId; emit q->lastReadEventChanged(u); // TODO: remove in 0.8 if (!isLocalUser(u)) -- cgit v1.2.3 From c79dcb673b6f2888faa4b62cd53689fde9f7ea10 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 1 Aug 2021 17:22:42 +0200 Subject: Room::toJson(): save the last local user's read receipt Read receipts are entangled with counting unread messages, and saving them also helps in not sending receipts for too old events. Other users' read receipts are still treated as truly ephemeral. --- lib/room.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e7d4e137..1c84ece0 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -3003,6 +3003,32 @@ QJsonObject Room::Private::toJson() const { QStringLiteral("events"), accountDataEvents } }); } + if (const auto& readReceiptEventId = + lastReadReceipts.value(q->localUser()).eventId; + !readReceiptEventId.isEmpty()) // + { + // Okay, that's a mouthful; but basically, it's simply placing an m.read + // event in the 'ephemeral' section of the cached sync payload. + // See also receiptevent.* and m.read example in the spec. + // Only the local user's read receipt is saved - others' are really + // considered ephemeral but this one is useful in understanding where + // the user is in the timeline before any history is loaded. + result.insert( + QStringLiteral("ephemeral"), + QJsonObject { + { QStringLiteral("events"), + QJsonArray { QJsonObject { + { TypeKey, ReceiptEvent::matrixTypeId() }, + { ContentKey, + QJsonObject { + { readReceiptEventId, + QJsonObject { + { QStringLiteral("m.read"), + QJsonObject { + { connection->userId(), + QJsonObject {} } } } } } } } } } } }); + } + QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, unreadMessages } }; -- cgit v1.2.3 From 4f08c88d234119c2a76874ebd2b1433b81992427 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 1 Aug 2021 17:47:56 +0200 Subject: Room::setDisplayed(): Don't reset unread counters Closes #489. --- lib/room.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1c84ece0..8462902f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -924,11 +924,8 @@ void Room::setDisplayed(bool displayed) d->displayed = displayed; emit displayedChanged(displayed); - if (displayed) { - resetHighlightCount(); - resetNotificationCount(); + if (displayed) d->getAllMembers(); - } } QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; } -- cgit v1.2.3 From 5276e4321c6b3348fc5b1d3d16f93131d9676e76 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 24 Jul 2021 21:06:13 +0200 Subject: Room::Private::sync/historyEdge() Move Room::sync/historyEdge() implementation to Room::Private, so that internal logic could use the same readable shortcuts without q-> prefixes, instead of timeline.crend() and timeline.cend() that are much less readable. --- lib/room.cpp | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 47a0ec94..9fe70678 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -101,8 +101,8 @@ public: UnorderedMap baseState; /// State event stubs - events without content, just type and state key static decltype(baseState) stubbedState; - /// The state of the room at timeline position after-maxTimelineIndex() - /// \sa Room::syncEdge + /// The state of the room at syncEdge() + /// \sa syncEdge QHash currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases @@ -198,6 +198,8 @@ public: /// A point in the timeline corresponding to baseState rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + rev_iter_t historyEdge() const { return timeline.crend(); } + Timeline::const_iterator syncEdge() const { return timeline.cend(); } void getPreviousContent(int limit = 10, const QString &filter = {}); @@ -638,7 +640,7 @@ void Room::Private::updateUnreadCount(const rev_iter_t& from, // unreadMessages and might need to promote the read marker further // over local-origin messages. auto readMarker = q->readMarker(); - if (readMarker == timeline.crend() && q->allHistoryLoaded()) + if (readMarker == historyEdge() && q->allHistoryLoaded()) --readMarker; // Read marker not found in the timeline, initialise it if (readMarker >= from && readMarker < to) { promoteReadMarker(q->localUser(), readMarker, true); @@ -682,7 +684,7 @@ Room::Changes Room::Private::promoteReadMarker(User* u, // iterators return Change::NoChange; - Q_ASSERT(newMarker < timeline.crend()); + Q_ASSERT(newMarker < historyEdge()); // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). @@ -697,7 +699,7 @@ Room::Changes Room::Private::promoteReadMarker(User* u, QElapsedTimer et; et.start(); unreadMessages = - int(count_if(eagerMarker, timeline.cend(), + int(count_if(eagerMarker, syncEdge(), [this](const auto& ti) { return isEventNotable(ti); })); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; @@ -771,12 +773,9 @@ bool Room::hasUnreadMessages() const { return unreadCount() >= 0; } int Room::unreadCount() const { return d->unreadMessages; } -Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } +Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); } -Room::Timeline::const_iterator Room::syncEdge() const -{ - return d->timeline.cend(); -} +Room::Timeline::const_iterator Room::syncEdge() const { return d->syncEdge(); } TimelineItem::index_t Room::minTimelineIndex() const { @@ -855,7 +854,7 @@ void Room::Private::getAllMembers() // the full members list was requested. if (!timeline.empty()) for (auto it = q->findInTimeline(nextIndex).base(); - it != timeline.cend(); ++it) + it != syncEdge(); ++it) if (is(**it)) roomChanges |= q->processStateEvent(**it); if (roomChanges & MembersChange) @@ -2357,7 +2356,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) emit q->aboutToAddNewMessages(eventsSpan); auto insertedSize = moveEventsToTimeline(eventsSpan, Newer); totalInserted += insertedSize; - auto firstInserted = timeline.cend() - insertedSize; + auto firstInserted = syncEdge() - insertedSize; q->onAddNewTimelineEvents(firstInserted); emit q->addedMessages(firstInserted->index(), timeline.back().index()); @@ -2387,20 +2386,20 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) { totalInserted += insertedSize; - q->onAddNewTimelineEvents(timeline.cend() - insertedSize); + q->onAddNewTimelineEvents(syncEdge() - insertedSize); } emit q->pendingEventMerged(); } // Events merged and transferred from `events` to `timeline` now. - const auto from = timeline.cend() - totalInserted; + const auto from = syncEdge() - totalInserted; if (q->supportsCalls()) - for (auto it = from; it != timeline.cend(); ++it) + for (auto it = from; it != syncEdge(); ++it) if (const auto* evt = it->viewAs()) emit q->callEvent(q, evt); if (totalInserted > 0) { - for (auto it = from; it != timeline.cend(); ++it) { + for (auto it = from; it != syncEdge(); ++it) { if (const auto* reaction = it->viewAs()) { const auto& relation = reaction->relation(); relations[{ relation.eventId, relation.type }] << reaction; @@ -2420,7 +2419,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // the new message events. if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) { auto* const firstWriter = q->user(senderId); - if (q->readMarker(firstWriter) != timeline.crend()) { + if (q->readMarker(firstWriter) != historyEdge()) { roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); qCDebug(MESSAGES) @@ -2461,14 +2460,14 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) emit q->aboutToAddHistoricalMessages(events); const auto insertedSize = moveEventsToTimeline(events, Older); - const auto from = timeline.crend() - insertedSize; + const auto from = historyEdge() - insertedSize; qCDebug(STATE) << "Room" << displayname << "received" << insertedSize << "past events; the oldest event is now" << timeline.front(); q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); - for (auto it = from; it != timeline.crend(); ++it) { + for (auto it = from; it != historyEdge(); ++it) { if (const auto* reaction = it->viewAs()) { const auto& relation = reaction->relation(); relations[{ relation.eventId, relation.type }] << reaction; @@ -2476,7 +2475,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) } } if (from <= q->readMarker()) - updateUnreadCount(from, timeline.crend()); + updateUnreadCount(from, historyEdge()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) -- cgit v1.2.3 From 776feaa5eeaffe78773aec9a4431fef147fa95c4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:21:06 +0200 Subject: Use Q_DECL_DEPRECATED_X instead of \deprecated doc-comment This still works with older moc yet produces actual warnings when compiling C++ code. --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 9fe70678..398b3ec8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1236,7 +1236,7 @@ QList Room::membersLeft() const { return d->membersLeft; } QList Room::users() const { return d->membersMap.values(); } -[[deprecated]] QStringList Room::memberNames() const +QStringList Room::memberNames() const { return safeMemberNames(); } @@ -1507,7 +1507,7 @@ QString Room::disambiguatedMemberName(const QString& mxId) const QString Room::safeMemberName(const QString& userId) const { - return sanitized(roomMembername(userId)); + return sanitized(disambiguatedMemberName(userId)); } QString Room::htmlSafeMemberName(const QString& userId) const -- cgit v1.2.3 From 6953e55361f600a591c08b9cd287a350230b3ef8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 23 Aug 2021 08:42:49 +0200 Subject: Room: isMember(); memberState() only needs user id Room::memberJoinState() was only used to check if the user has joined the room (it couldn't be used for anything else), meaning that its best replacement is actually not memberState() but isMember() introduced hereby. It's also better to pass user ids instead of User objects to memberState() and isMember() since that is enough to check membership. # Conflicts: # lib/room.cpp # lib/room.h --- lib/room.cpp | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 398b3ec8..45f1af53 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -590,9 +590,14 @@ JoinState Room::memberJoinState(User* user) const : JoinState::Leave; } -Membership Room::memberState(User* user) const +Membership Room::memberState(const QString& userId) const { - return d->getCurrentState(user->id())->membership(); + return d->getCurrentState(userId)->membership(); +} + +bool Room::isMember(const QString& userId) const +{ + return memberState(userId) == Membership::Join; } JoinState Room::joinState() const { return d->joinState; } @@ -2698,11 +2703,10 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) et.start(); if (auto* evt = eventCast(event)) { d->usersTyping.clear(); - for (const auto& userId : evt->users()) { - auto u = user(userId); - if (memberJoinState(u) == JoinState::Join) - d->usersTyping.append(u); - } + for (const auto& userId : evt->users()) + if (isMember(userId)) + d->usersTyping.append(user(userId)); + if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" << evt->users().size() << "users," << et; @@ -2726,9 +2730,9 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 - auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join) - changes |= d->promoteReadMarker(u, newMarker); + if (isMember(r.userId)) + changes |= + d->promoteReadMarker(user(r.userId), newMarker); } } else { qCDebug(EPHEMERAL) << "Event" << p.evtId @@ -2740,9 +2744,10 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 + if (!isMember(r.userId)) + continue; auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join - && readMarker(u) == historyEdge()) + if (readMarker(u) == historyEdge()) changes |= d->setLastReadEvent(u, p.evtId); } } -- cgit v1.2.3 From 06a8ef6ebed5962117121486059ba46dc7f6d4f9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 30 Aug 2021 12:35:27 +0200 Subject: Room: displayNameForHtml This is useful for cases when the room display name is returned to QML that doesn't have an equivalent of QString::toHtmlEscaped(). --- lib/room.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 45f1af53..890da13d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -549,6 +549,11 @@ QString Room::canonicalAlias() const QString Room::displayName() const { return d->displayname; } +QString Room::displayNameForHtml() const +{ + return displayName().toHtmlEscaped(); +} + void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const -- cgit v1.2.3 From 9e548e0625a819052cd10d5c4bf129dde649a6a4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Sep 2021 19:27:57 +0200 Subject: Straighten up file transfer cancellation There was a mess with fileTransferCancelled(); it was only emitted when a download (but not an upload) was cancelled; besides, in case of downloads a file transfer info structure was getting deleted whereas uploads left a file transfer in Cancelled status. This all now converges on: - fileTransferFailed() for both failures and cancellations (to simplify slot connection, and also to follow the practice in, e.g., Qt Network). - the file transfer info structure is kept around in Cancelled status, following the logic used for failures. There's no particular cleanup which may become a problem if one uploads and cancels many times (download file transfers are keyed to event ids, mitigating the problem); this will be fixed in another commit. Closes #503. Closes #504. --- lib/room.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 890da13d..c6cca2ea 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1708,6 +1708,12 @@ QString Room::retryMessage(const QString& txnId) return d->doSendEvent(it->event()); } +// Lambda defers actual tr() invocation to the moment when translations are +// initialised +const auto FileTransferCancelledMsg = [] { + return Room::tr("File transfer cancelled"); +}; + void Room::discardMessage(const QString& txnId) { auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), @@ -1722,7 +1728,7 @@ void Room::discardMessage(const QString& txnId) if (isJobPending(transferIt->job)) { transferIt->status = FileTransferInfo::Cancelled; transferIt->job->abandon(); - emit fileTransferFailed(txnId, tr("File upload cancelled")); + emit fileTransferFailed(txnId, FileTransferCancelledMsg()); } else if (transferIt->status == FileTransferInfo::Completed) { qCWarning(MAIN) << "File for transaction" << txnId @@ -1790,7 +1796,7 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) "cancelled"; } }); - connect(q, &Room::fileTransferCancelled, transferJob, + connect(q, &Room::fileTransferFailed, transferJob, [this, txnId](const QString& tId) { if (tId != txnId) return; @@ -2079,16 +2085,16 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) void Room::cancelFileTransfer(const QString& id) { - const auto it = d->fileTransfers.constFind(id); - if (it == d->fileTransfers.cend()) { + const auto it = d->fileTransfers.find(id); + if (it == d->fileTransfers.end()) { qCWarning(MAIN) << "No information on file transfer" << id << "in room" << d->id; return; } if (isJobPending(it->job)) it->job->abandon(); - d->fileTransfers.remove(id); - emit fileTransferCancelled(id); + it->status = FileTransferInfo::Cancelled; + emit fileTransferFailed(id, FileTransferCancelledMsg()); } void Room::Private::dropDuplicateEvents(RoomEvents& events) const -- cgit v1.2.3 From 2bf18a64d236c2364e12d4c2f1a9464cc6a2ebf9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 10 Sep 2021 22:38:10 +0200 Subject: Move URL creation to Room/Connection; use query instead of fragment The query is easier to manipulate; and the original mxc URL is not used for the real network request anyway. --- lib/room.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 890da13d..72b37f62 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1116,6 +1116,17 @@ QList Room::directChatUsers() const return connection()->directChatUsers(this); } +QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const +{ + auto url = connection()->makeMediaUrl(mxcUrl); + QUrlQuery q(url.query()); + Q_ASSERT(q.hasQueryItem("user_id")); + q.addQueryItem("room_id", id()); + q.addQueryItem("event_id", eventId); + url.setQuery(q); + return url; +} + QString safeFileName(QString rawName) { return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_"); -- cgit v1.2.3 From 0209be8305aa38722a3d25593ae71fbb3ac05e52 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 11 Sep 2021 16:18:35 +0200 Subject: Add convenience function for activating encryption and fix EncryptionEvent constructor --- lib/room.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c6cca2ea..ced313a4 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -3009,3 +3009,12 @@ bool MemberSorter::operator()(User* u1, QStringView u2name) const return n1.localeAwareCompare(n2) < 0; } + +void Room::activateEncryption() +{ + if(usesEncryption()) { + qCWarning(E2EE) << "Room" << objectName() << "is already encrypted"; + return; + } + setState(EncryptionEventContent::MegolmV1AesSha2); +} -- cgit v1.2.3 From f89285a3bd7c093622be966823d0ae1254822905 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 18:48:12 +0200 Subject: Room: use more modern Connection API --- lib/room.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index fb65fd84..a923bf9a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1180,9 +1180,8 @@ QUrl Room::urlToThumbnail(const QString& eventId) const if (event->hasThumbnail()) { auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); - return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(), - thumbnail->url, - thumbnail->imageSize); + return connection()->getUrlForApi( + thumbnail->url, thumbnail->imageSize); } qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; @@ -1193,8 +1192,7 @@ QUrl Room::urlToDownload(const QString& eventId) const if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); - return DownloadFileJob::makeRequestUrl(connection()->homeserver(), - fileInfo->url); + return connection()->getUrlForApi(fileInfo->url); } return {}; } -- cgit v1.2.3 From fe77279cf0f7de006db40a828cc2fb7261e8e0bd Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 10 Oct 2021 00:07:44 +0200 Subject: Keep the reply when replacing an event --- lib/room.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a923bf9a..e1d41fc3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2251,8 +2251,13 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) RoomEventPtr makeReplaced(const RoomEvent& target, const RoomMessageEvent& replacement) { + const auto &targetReply = target.contentJson()["m.relates_to"].toObject(); + auto newContent = replacement.contentJson().value("m.new_content"_ls).toObject(); + if (!targetReply.empty()) { + newContent["m.relates_to"] = targetReply; + } auto originalJson = target.originalJsonObject(); - originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls); + originalJson[ContentKeyL] = newContent; auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); auto relations = unsignedData.take("m.relations"_ls).toObject(); -- cgit v1.2.3 From 5c0346f3a700e6af31463490b7af3382b86e09d5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 8 Sep 2021 05:47:10 +0200 Subject: Room: actually initialise read marker when needed This fixes the `q->readMarker() != historyEdge()` assertion failure occuring in recalculateUnreadCount() when new events from sync arrive to a room with no read marker and all history loaded. --- lib/room.cpp | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a2b99039..ea8df286 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -686,23 +686,24 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, if (fullyReadMarker < from) return NoChange; // What's arrived is already fully read + // If there's no read marker in the whole room, initialise it if (fullyReadMarker == historyEdge() && q->allHistoryLoaded()) - --fullyReadMarker; // No read marker in the whole room, initialise it - if (fullyReadMarker < to) { - // Catch a special case when the last fully read event id refers to an - // event that has just arrived. In this case we should recalculate - // unreadMessages to get an exact number instead of an estimation - // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). - // For the same reason (switching from the estimation to the exact - // number) this branch always emits unreadMessagesChanged() and returns - // UnreadNotifsChange, even if the estimation luckily matched the exact - // result. + return setFullyReadMarker(timeline.front()->id()); + + // Catch a special case when the last fully read event id refers to an + // event that has just arrived. In this case we should recalculate + // unreadMessages to get an exact number instead of an estimation + // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). + // For the same reason (switching from the estimation to the exact + // number) this branch always emits unreadMessagesChanged() and returns + // UnreadNotifsChange, even if the estimation luckily matched the exact + // result. + if (fullyReadMarker < to) return recalculateUnreadCount(true); - } - // Fully read marker is somewhere beyond the most historical message from - // the arrived batch - add up newly arrived messages to the current counter, - // instead of a complete recalculation. + // At this point the fully read marker is somewhere beyond the "oldest" + // message from the arrived batch - add up newly arrived messages to + // the current counter, instead of a complete recalculation. Q_ASSERT(to <= fullyReadMarker); QElapsedTimer et; @@ -769,7 +770,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // - << "moved to" << fullyReadUntilEventId; + << "set to" << fullyReadUntilEventId; emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); // TODO: Remove in 0.8 emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); -- cgit v1.2.3 From 965f0e94f3f8c98ccb704b1d5abdeac1efc699cc Mon Sep 17 00:00:00 2001 From: Smitty Date: Sun, 7 Nov 2021 17:08:53 -0500 Subject: Add method to get all state events in a room It is useful to be able to get all of the state events that have occured in a room, instead of needing to use Room::getCurrentState, which filters based on the event type and state key. --- lib/room.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e1d41fc3..3b00d2d9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -224,6 +224,11 @@ public: return evt; } + const QHash stateEvents() const + { + return currentState; + } + template const EventT* getCurrentState(const QString& stateKey = {}) const { @@ -1293,6 +1298,11 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } +const QHash Room::stateEvents() const +{ + return d->stateEvents(); +} + RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED -- cgit v1.2.3 From 48400a8aa28537c1bedc095b5c4c08f677cca0f5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 8 Nov 2021 14:47:18 +0100 Subject: Drop unused #include --- lib/room.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e1d41fc3..4e7b29d8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -54,7 +54,6 @@ #include #include -#include #include #include #include // for efficient string concats (operator%) -- cgit v1.2.3 From 38291a7b442906b638ce0d34a429c72089e80f6d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 8 Nov 2021 19:27:46 +0100 Subject: Room::Change: deprecate AccountDataChange, ReadMarkerChange These usually don't affect the room outlooks in the room list in any way; so can be merged into OtherChange instead. Also: OtherChanges synonym has been added, implying that there might be more than one change behind a single "Other" flag. --- lib/room.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4e7b29d8..6bccc405 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -633,7 +633,9 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) connection->callApi(BackgroundRequest, id, storedId); emit q->readMarkerMoved(eventId, storedId); - return Change::ReadMarkerChange; + // TODO: Drop ReadMarkerChange in 0.8 + return QT_IGNORE_DEPRECATIONS(Change::ReadMarkerChange) + | Change::OtherChange; } return Change::NoChange; } @@ -2813,7 +2815,8 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(STATE) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); - changes |= Change::AccountDataChange; + // TODO: Drop AccountDataChange in 0.8 + QT_IGNORE_DEPRECATIONS(changes |= AccountDataChange|OtherChange); } return changes; } -- cgit v1.2.3 From 29195491c25bcba50b78a30c779db07913f87d3a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 11 Nov 2021 13:01:58 +0100 Subject: Update comments around read receipts code --- lib/room.cpp | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ceb8d111..4d11878f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2498,12 +2498,6 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << totalInserted << "new events; the last event is now" << timeline.back(); - // The first event in the just-added batch (referred to by `from`) - // defines whose read receipt can possibly be promoted any further over - // the same author's events newly arrived. Others will need explicit - // read receipts from the server - or, for the local user, calling - // setLastDisplayedEventId() - to promote their read receipts over - // the new message events. auto* const firstWriter = q->user((*from)->senderId()); setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); if (firstWriter == q->localUser() @@ -2813,15 +2807,14 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) if (newMarker == historyEdge()) qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " "found; saving them anyway"; + // If the event is not found (most likely, because it's too old and + // hasn't been fetched from the server yet) but there is a previous + // marker for a user, keep the previous marker because read receipts + // are not supposed to move backwards. Otherwise, blindly store + // the event id for this user and update the read marker when/if + // the event is fetched later on. for (const Receipt& r : p.receipts) if (isMember(r.userId)) { - // If the event is not found (most likely, because it's - // too old and hasn't been fetched from the server yet) - // but there is a previous marker for a user, keep - // the previous marker because read receipts are not - // supposed to move backwards. Otherwise, blindly - // store the event id for this user and update the read - // marker when/if the event is fetched later on. d->setLastReadReceipt(user(r.userId), newMarker, { p.evtId, r.timestamp }); } -- cgit v1.2.3 From fb30d455e2dbeed8c242cfbeb153f834b0358f11 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 12 Nov 2021 11:10:30 +0100 Subject: Fix building with GCC It didn't like using QT_IGNORE_DEPRECATIONS inside a statement. --- lib/room.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6bccc405..c11f7990 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -633,9 +633,12 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) connection->callApi(BackgroundRequest, id, storedId); emit q->readMarkerMoved(eventId, storedId); - // TODO: Drop ReadMarkerChange in 0.8 - return QT_IGNORE_DEPRECATIONS(Change::ReadMarkerChange) - | Change::OtherChange; + + // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around + // a statement, not within a statement + QT_IGNORE_DEPRECATIONS( // TODO: Drop ReadMarkerChange in 0.8 + return Change::ReadMarkerChange | Change::OtherChange; + ) } return Change::NoChange; } @@ -2816,7 +2819,9 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); // TODO: Drop AccountDataChange in 0.8 - QT_IGNORE_DEPRECATIONS(changes |= AccountDataChange|OtherChange); + // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around + // a statement, not within a statement + QT_IGNORE_DEPRECATIONS(changes |= AccountDataChange | OtherChange;) } return changes; } -- cgit v1.2.3 From f2bf3f203965c51824e8681427798f7a09784ce3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 11 Nov 2021 22:18:18 +0100 Subject: Make ReceiptEvent constructible from content Makes the Room::P::toJson() code more readable. --- lib/room.cpp | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4d11878f..58950aac 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2813,7 +2813,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) // are not supposed to move backwards. Otherwise, blindly store // the event id for this user and update the read marker when/if // the event is fetched later on. - for (const Receipt& r : p.receipts) + for (const auto& r : p.receipts) if (isMember(r.userId)) { d->setLastReadReceipt(user(r.userId), newMarker, { p.evtId, r.timestamp }); @@ -3021,30 +3021,17 @@ QJsonObject Room::Private::toJson() const { QStringLiteral("events"), accountDataEvents } }); } - if (const auto& readReceiptEventId = - lastReadReceipts.value(q->localUser()).eventId; - !readReceiptEventId.isEmpty()) // + if (const auto& readReceipt = q->lastReadReceipt(connection->userId()); + !readReceipt.eventId.isEmpty()) // { - // Okay, that's a mouthful; but basically, it's simply placing an m.read - // event in the 'ephemeral' section of the cached sync payload. - // See also receiptevent.* and m.read example in the spec. - // Only the local user's read receipt is saved - others' are really - // considered ephemeral but this one is useful in understanding where - // the user is in the timeline before any history is loaded. result.insert( QStringLiteral("ephemeral"), QJsonObject { { QStringLiteral("events"), - QJsonArray { QJsonObject { - { TypeKey, ReceiptEvent::matrixTypeId() }, - { ContentKey, - QJsonObject { - { readReceiptEventId, - QJsonObject { - { QStringLiteral("m.read"), - QJsonObject { - { connection->userId(), - QJsonObject {} } } } } } } } } } } }); + QJsonArray { ReceiptEvent({ { readReceipt.eventId, + { { connection->userId(), + readReceipt.timestamp } } } }) + .fullJson() } } }); } QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, -- cgit v1.2.3 From d8c9f0d5802f8b1c3ef362964dac5c8d60a61871 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 15 Nov 2021 23:03:09 +0100 Subject: Port away from deprecated upfront percent encoding --- lib/room.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c11f7990..c2c4ba78 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -750,8 +750,7 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) if ((*upToMarker)->senderId() != q->localUser()->id()) { connection->callApi(BackgroundRequest, id, QStringLiteral("m.read"), - QUrl::toPercentEncoding( - (*upToMarker)->id())); + (*upToMarker)->id()); break; } } -- cgit v1.2.3 From cb7b9e8d04f060893a5ffb8cfa22c627c7dbe507 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 16 Nov 2021 00:53:39 +0100 Subject: Port away from implicit 'this' captures in lambdas Deprecated with C++20 --- lib/room.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c11f7990..bef06dfe 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -572,13 +572,13 @@ QImage Room::avatar(int width, int height) { if (!d->avatar.url().isEmpty()) return d->avatar.get(connection(), width, height, - [=] { emit avatarChanged(); }); + [this] { emit avatarChanged(); }); // Use the first (excluding self) user's avatar for direct chats const auto dcUsers = directChatUsers(); for (auto* u : dcUsers) if (u != localUser()) - return u->avatar(width, height, this, [=] { emit avatarChanged(); }); + return u->avatar(width, height, this, [this] { emit avatarChanged(); }); return {}; } @@ -861,7 +861,7 @@ void Room::Private::getAllMembers() allMembersJob = connection->callApi( id, connection->nextBatchToken(), "join"); auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; - connect(allMembersJob, &BaseJob::success, q, [=] { + connect(allMembersJob, &BaseJob::success, q, [this, nextIndex] { Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); auto roomChanges = updateStateFrom(allMembersJob->chunk()); // Replay member events that arrived after the point for which @@ -1973,7 +1973,7 @@ void Room::Private::getPreviousContent(int limit, const QString &filter) eventsHistoryJob = connection->callApi(id, prevBatch, "b", "", limit, filter); emit q->eventsHistoryJobChanged(); - connect(eventsHistoryJob, &BaseJob::success, q, [=] { + connect(eventsHistoryJob, &BaseJob::success, q, [this] { prevBatch = eventsHistoryJob->end(); addHistoricalMessageEvents(eventsHistoryJob->chunk()); }); -- cgit v1.2.3 From edb63528e6f3048045f70eb6a48412917bdbea0b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 15 Nov 2021 21:37:04 +0100 Subject: Bind read receipts to userIds, not to User* values This reduces the surface interacting with the User class that eventually will be split into LocalUser (most part) and RoomMember (a tiny wrapper around the member data in a given room, used almost everywhere in Room where User currently is). Also: dropped a log message when the new receipt is at or behind the old one as it causes a lot of noise in the logs. --- lib/room.cpp | 163 +++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 90 insertions(+), 73 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 58950aac..ee76a85c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -120,14 +120,14 @@ public: int notificationCount = 0; members_map_t membersMap; QList usersTyping; - QHash> eventIdReadUsers; + QHash> eventIdReadUsers; QList usersInvited; QList membersLeft; int unreadMessages = 0; bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; - QHash lastReadReceipts; + QHash lastReadReceipts; QString fullyReadUntilEventId; TagsMap tags; UnorderedMap accountData; @@ -291,7 +291,7 @@ public: */ void dropDuplicateEvents(RoomEvents& events) const; - void setLastReadReceipt(User* u, rev_iter_t newMarker, + bool setLastReadReceipt(const QString& userId, rev_iter_t newMarker, ReadReceipt newReceipt = {}); Changes setFullyReadMarker(const QString &eventId); Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); @@ -619,60 +619,62 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, +bool Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, ReadReceipt newReceipt) { - if (!u) { - Q_ASSERT(u != nullptr); // For Debug builds - qCCritical(MAIN) << "Empty user, skipping read receipt registration"; - return; // For Release builds - } - if (!q->isMember(u->id())) { - qCWarning(EPHEMERAL) - << "Won't record read receipt for non-member" << u->id(); - return; - } - - auto& storedReceipt = lastReadReceipts[u]; - if (newMarker == timeline.crend() && !newReceipt.eventId.isEmpty()) + if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) newMarker = q->findInTimeline(newReceipt.eventId); - if (newMarker != timeline.crend()) { - // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->findInTimeline(storedReceipt.eventId)) { - qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() - << "is at or behind the old one, skipping"; - return; - } - + if (newMarker != historyEdge()) { // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). - const auto eagerMarker = find_if(newMarker.base(), timeline.cend(), + const auto eagerMarker = find_if(newMarker.base(), syncEdge(), [=](const TimelineItem& ti) { - return ti->senderId() != u->id(); - }) - - 1; - newReceipt.eventId = (*eagerMarker)->id(); - if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt - qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id() + return ti->senderId() != userId; + }); + // eagerMarker is now just after the desired event for newMarker + if (eagerMarker != newMarker.base()) { + qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId << "to" << newReceipt.eventId; - } + newMarker = rev_iter_t(eagerMarker); + } + // Fill newReceipt with the event (and, if needed, timestamp) from + // eagerMarker + newReceipt.eventId = (eagerMarker - 1)->event()->id(); + if (newReceipt.timestamp.isNull()) + newReceipt.timestamp = QDateTime::currentDateTime(); + } + auto& storedReceipt = + lastReadReceipts[userId]; // clazy:exclude=detaching-member + const auto prevEventId = storedReceipt.eventId; + // NB: with reverse iterators, timeline history >= sync edge + if (newMarker >= q->findInTimeline(prevEventId)) + return false; - if (storedReceipt == newReceipt) - return; // Finally make the change - auto& oldEventReadUsers = eventIdReadUsers[storedReceipt.eventId]; - oldEventReadUsers.remove(u); - if (oldEventReadUsers.isEmpty()) - eventIdReadUsers.remove(storedReceipt.eventId); - eventIdReadUsers[newReceipt.eventId].insert(u); - swap(storedReceipt, newReceipt); // Now newReceipt actually stores the old receipt - qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at" + + auto oldEventReadUsersIt = + eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member + if (oldEventReadUsersIt != eventIdReadUsers.end()) { + oldEventReadUsersIt->remove(userId); + if (oldEventReadUsersIt->isEmpty()) + eventIdReadUsers.erase(oldEventReadUsersIt); + } + eventIdReadUsers[newReceipt.eventId].insert(userId); + storedReceipt = move(newReceipt); + qCDebug(EPHEMERAL) << "The new read receipt for" << userId << "is now at" << storedReceipt.eventId; - emit q->lastReadEventChanged(u); + + // TODO: use Room::member() when it becomes a thing and only emit signals + // for actual members, not just any user + const auto member = q->user(userId); + Q_ASSERT(member != nullptr); + emit q->lastReadEventChanged(member); // TODO: remove in 0.8 - if (!isLocalUser(u)) - emit q->readMarkerForUserMoved(u, newReceipt.eventId, + if (!isLocalUser(member)) + emit q->readMarkerForUserMoved(member, prevEventId, storedReceipt.eventId); + return true; } Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, @@ -777,7 +779,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) Changes changes = ReadMarkerChange; if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { // Pull read receipt if it's behind - setLastReadReceipt(q->localUser(), rm); + setLastReadReceipt(connection->userId(), rm); changes |= recalculateUnreadCount(); // TODO: updateUnreadCount()? } return changes; @@ -785,8 +787,13 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) void Room::setReadReceipt(const QString& atEventId) { - d->setLastReadReceipt(localUser(), historyEdge(), - { atEventId, QDateTime::currentDateTime() }); + if (!d->setLastReadReceipt(localUser()->id(), historyEdge(), + { atEventId, QDateTime::currentDateTime() })) { + qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id() + << "in" << objectName() + << "is at or behind the old one, skipping"; + return; + } connection()->callApi(BackgroundRequest, id(), QStringLiteral("m.read"), QUrl::toPercentEncoding(atEventId)); @@ -1004,7 +1011,7 @@ QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } ReadReceipt Room::lastReadReceipt(const QString& userId) const { - return d->lastReadReceipts.value(user(userId)); + return d->lastReadReceipts.value(userId); } QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } @@ -1014,11 +1021,21 @@ Room::rev_iter_t Room::fullyReadMarker() const return findInTimeline(d->fullyReadUntilEventId); } -QSet Room::usersAtEventId(const QString& eventId) +QSet Room::userIdsAtEvent(const QString& eventId) { return d->eventIdReadUsers.value(eventId); } +QSet Room::usersAtEventId(const QString& eventId) +{ + const auto& userIds = d->eventIdReadUsers.value(eventId); + QSet users; + users.reserve(userIds.size()); + for (const auto& uId : userIds) + users.insert(user(uId)); + return users; +} + int Room::notificationCount() const { return d->notificationCount; } void Room::resetNotificationCount() @@ -2498,17 +2515,16 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << totalInserted << "new events; the last event is now" << timeline.back(); - auto* const firstWriter = q->user((*from)->senderId()); - setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); - if (firstWriter == q->localUser() + const auto& firstWriterId = (*from)->senderId(); + setLastReadReceipt(firstWriterId, rev_iter_t(from + 1)); + // If the local user's message(s) is/are first in the batch + // and the fully read marker was right before it, promote + // the fully read marker to the same event as the read receipt. + if (firstWriterId == connection->userId() && q->fullyReadMarker().base() == from) // - { - // If the local user's message(s) is/are first in the batch - // and the fully read marker was right before it, promote - // the fully read marker to the same event as the read receipt. roomChanges |= - setFullyReadMarker(lastReadReceipts.value(firstWriter).eventId); - } + setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId); + roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } @@ -2781,6 +2797,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) et.start(); if (auto* evt = eventCast(event)) { d->usersTyping.clear(); + d->usersTyping.reserve(evt->users().size()); // Assume all are members for (const auto& userId : evt->users()) if (isMember(userId)) d->usersTyping.append(user(userId)); @@ -2795,29 +2812,29 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) const auto& eventsWithReceipts = evt->eventsWithReceipts(); for (const auto& p : eventsWithReceipts) { totalReceipts += p.receipts.size(); - { - if (p.receipts.size() == 1) - qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for" - << p.receipts[0].userId; - else - qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for" - << p.receipts.size() << "users"; - } const auto newMarker = findInTimeline(p.evtId); if (newMarker == historyEdge()) qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " - "found; saving them anyway"; + "found; saving anyway"; // If the event is not found (most likely, because it's too old and // hasn't been fetched from the server yet) but there is a previous // marker for a user, keep the previous marker because read receipts // are not supposed to move backwards. Otherwise, blindly store // the event id for this user and update the read marker when/if // the event is fetched later on. - for (const auto& r : p.receipts) - if (isMember(r.userId)) { - d->setLastReadReceipt(user(r.userId), newMarker, - { p.evtId, r.timestamp }); - } + const auto updatedCount = std::count_if( + p.receipts.cbegin(), p.receipts.cend(), + [this, &newMarker, &evtId = p.evtId](const auto& r) -> bool { + return d->setLastReadReceipt(r.userId, newMarker, + { evtId, r.timestamp }); + }); + + if (p.receipts.size() > 1) + qCDebug(EPHEMERAL) << p.evtId << "marked as read for" + << updatedCount << "user(s)"; + if (updatedCount < p.receipts.size()) + qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount + << "receipts were skipped"; } if (eventsWithReceipts.size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) -- cgit v1.2.3 From b472fc355dc5ce70391ca2b9bc8da35b973ae3a3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 8 Nov 2021 21:03:37 +0100 Subject: Room: lastLocalReadReceipt(), localReadReceiptMarker() To simplify retrieval of the local m.read receipt and the marker for it. --- lib/room.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ee76a85c..273a5753 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1014,6 +1014,16 @@ ReadReceipt Room::lastReadReceipt(const QString& userId) const return d->lastReadReceipts.value(userId); } +ReadReceipt Room::lastLocalReadReceipt() const +{ + return d->lastReadReceipts.value(localUser()->id()); +} + +Room::rev_iter_t Room::localReadReceiptMarker() const +{ + return findInTimeline(lastLocalReadReceipt().eventId); +} + QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } Room::rev_iter_t Room::fullyReadMarker() const -- cgit v1.2.3 From d97195d3c67dcf08d727a2a65863b99982c6b24e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 10 Jul 2021 18:37:16 +0200 Subject: Room: refactoring around logging (esp. profile logs) --- lib/room.cpp | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a06a8d2a..54d63138 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -266,8 +266,8 @@ public: } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) - << "*** Room::Private::updateStateFrom():" << events.size() - << "event(s)," << et; + << "Updated" << q->objectName() << "room state from" + << events.size() << "event(s) in" << et; } return changes; } @@ -713,7 +713,8 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Counting gained unread messages took" << et; + qCDebug(PROFILER) << "Counting gained unread messages in" + << q->objectName() << "took" << et; if (newUnreadMessages == 0) return NoChange; @@ -745,7 +746,8 @@ Room::Changes Room::Private::recalculateUnreadCount(bool force) int(count_if(timeline.crbegin(), q->fullyReadMarker(), [this](const auto& ti) { return isEventNotable(ti); })); if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Recounting unread messages took" << et; + qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName() + << "took" << et; // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages == 0) @@ -1634,21 +1636,13 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) setJoinState(data.joinState); Changes roomChanges = Change::NoChange; - QElapsedTimer et; - et.start(); for (auto&& event : data.accountData) roomChanges |= processAccountDataEvent(move(event)); roomChanges |= d->updateStateFrom(data.state); + // The order of calculation is important - don't merge these lines! + roomChanges |= d->addNewMessageEvents(move(data.timeline)); - if (!data.timeline.empty()) { - et.restart(); - roomChanges |= d->addNewMessageEvents(move(data.timeline)); - if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) - << "*** Room::addNewMessageEvents():" << data.timeline.size() - << "event(s)," << et; - } if (roomChanges & TopicChange) emit topicChanged(); @@ -2403,6 +2397,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (events.empty()) return Change::NoChange; + QElapsedTimer et; + et.start(); { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -2539,6 +2535,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + if (totalInserted > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "Added" << totalInserted << "new event(s) to" + << q->objectName() << "in" << et; return roomChanges; } @@ -2583,13 +2582,13 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) updateUnreadCount(from, historyEdge()); // When there are no unread messages and the read marker is within the // known timeline, unreadMessages == -1 - // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). + // (see https://github.com/quotient-im/libQuotient/wiki/Developer-notes#2-saving-unread-event-counts). Q_ASSERT(unreadMessages != 0 || q->fullyReadMarker() == historyEdge()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():" - << insertedSize << "event(s)," << et; + qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to" + << q->objectName() << "in" << et; } Room::Changes Room::processStateEvent(const RoomEvent& e) @@ -2813,8 +2812,9 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) d->usersTyping.append(user(userId)); if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" - << evt->users().size() << "users," << et; + qCDebug(PROFILER) + << "Processing typing events from" << evt->users().size() + << "user(s) in" << objectName() << "took" << et; emit typingChanged(); } if (auto* evt = eventCast(event)) { @@ -2848,10 +2848,9 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) } if (eventsWithReceipts.size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) - << "*** Room::processEphemeralEvent(receipts):" - << eventsWithReceipts.size() << "event(s) with" - << totalReceipts << "receipt(s)," << et; + qCDebug(PROFILER) << "Processing" << totalReceipts + << "receipt(s) on" << eventsWithReceipts.size() + << "event(s) in" << objectName() << "took" << et; } return changes; } @@ -3075,7 +3074,8 @@ QJsonObject Room::Private::toJson() const result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); if (et.elapsed() > 30) - qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; + qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took" + << et; return result; } -- cgit v1.2.3 From 76b6238af16a1ccd284831ce42ec9e2cb1fba2c5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 18 Nov 2021 17:33:05 +0100 Subject: Make Room::Changes an enum class; simplify enumerators This enumeration sees very limited (if any) use outside Quotient; and though this change will surely break code using it the fix is very straightforward and quick. --- lib/room.cpp | 70 +++++++++++++++++++++++++++++------------------------------- 1 file changed, 34 insertions(+), 36 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 54d63138..a376238e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -251,15 +251,14 @@ public: template Changes updateStateFrom(EventArrayT&& events) { - Changes changes = NoChange; + Changes changes {}; if (!events.empty()) { QElapsedTimer et; et.start(); for (auto&& eptr : events) { const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); - auto change = q->processStateEvent(evt); - if (change != NoChange) { + if (auto change = q->processStateEvent(evt); change) { changes |= change; baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); } @@ -615,7 +614,6 @@ void Room::setJoinState(JoinState state) d->joinState = state; qCDebug(STATE) << "Room" << id() << "changed state: " << oldState << "->" << state; - emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } @@ -685,7 +683,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, auto fullyReadMarker = q->fullyReadMarker(); if (fullyReadMarker < from) - return NoChange; // What's arrived is already fully read + return Change::None; // What's arrived is already fully read // If there's no read marker in the whole room, initialise it if (fullyReadMarker == historyEdge() && q->allHistoryLoaded()) @@ -717,7 +715,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, << q->objectName() << "took" << et; if (newUnreadMessages == 0) - return NoChange; + return Change::None; // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages < 0) @@ -731,7 +729,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, : "in total") << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); - return UnreadNotifsChange; + return Change::UnreadNotifs; } Room::Changes Room::Private::recalculateUnreadCount(bool force) @@ -754,7 +752,7 @@ Room::Changes Room::Private::recalculateUnreadCount(bool force) unreadMessages = -1; if (!force && unreadMessages == oldUnreadCount) - return NoChange; + return Change::None; if (unreadMessages == -1) qCDebug(MESSAGES) @@ -763,13 +761,13 @@ Room::Changes Room::Private::recalculateUnreadCount(bool force) qCDebug(MESSAGES) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); - return UnreadNotifsChange; + return Change::UnreadNotifs; } Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) { if (fullyReadUntilEventId == eventId) - return NoChange; + return Change::None; const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // @@ -778,7 +776,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) // TODO: Remove in 0.8 emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - Changes changes = ReadMarkerChange; + QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker;) if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { // Pull read receipt if it's behind setLastReadReceipt(connection->userId(), rm); @@ -927,7 +925,7 @@ void Room::Private::getAllMembers() it != syncEdge(); ++it) if (is(**it)) roomChanges |= q->processStateEvent(**it); - if (roomChanges & MembersChange) + if (roomChanges & Change::Members) emit q->memberListChanged(); emit q->allMembersLoaded(); }); @@ -1442,11 +1440,11 @@ GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; } Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { if (!summary.merge(newSummary)) - return Change::NoChange; + return Change::None; qCDebug(STATE).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; emit q->memberListChanged(); - return Change::SummaryChange; + return Change::Summary; } void Room::Private::insertMemberIntoMap(User* u) @@ -1635,7 +1633,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); - Changes roomChanges = Change::NoChange; + Changes roomChanges {}; for (auto&& event : data.accountData) roomChanges |= processAccountDataEvent(move(event)); @@ -1643,13 +1641,13 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) // The order of calculation is important - don't merge these lines! roomChanges |= d->addNewMessageEvents(move(data.timeline)); - if (roomChanges & TopicChange) + if (roomChanges & Change::Topic) emit topicChanged(); - if (roomChanges & (NameChange | AliasesChange)) + if (roomChanges & (Change::Name | Change::Aliases)) emit namesChanged(this); - if (roomChanges & MembersChange) + if (roomChanges & Change::Members) emit memberListChanged(); roomChanges |= d->setSummary(move(data.summary)); @@ -1670,7 +1668,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (merge(d->notificationCount, data.notificationCount)) emit notificationCountChanged(); - if (roomChanges != Change::NoChange) { + if (roomChanges) { d->updateDisplayname(); emit changed(roomChanges); if (!fromCache) @@ -2395,7 +2393,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return Change::NoChange; + return Change::None; QElapsedTimer et; et.start(); @@ -2448,7 +2446,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // clients historically expect. This may eventually change though if we // postulate that the current state is only current between syncs but not // within a sync. - Changes roomChanges = Change::NoChange; + Changes roomChanges {}; for (const auto& eptr : events) roomChanges |= q->processStateEvent(*eptr); @@ -2594,7 +2592,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) Room::Changes Room::processStateEvent(const RoomEvent& e) { if (!e.isStateEvent()) - return NoChange; + return Change::None; // Find a value (create an empty one if necessary) and get a reference // to it. Can't use getCurrentState<>() because it (creates and) returns @@ -2682,7 +2680,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) , true); // By default, go forward with the state change // clang-format on if (!proceed) - return NoChange; + return Change::None; // Change the state const auto* const oldStateEvent = @@ -2700,7 +2698,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // clang-format off const auto result = visit(e , [] (const RoomNameEvent&) { - return NameChange; + return Change::Name; } , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { // clang-format on @@ -2719,16 +2717,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) newAliases.push_front(cae.alias()); connection()->updateRoomAliases(id(), previousAltAliases, newAliases); - return AliasesChange; + return Change::Aliases; // clang-format off } , [] (const RoomTopicEvent&) { - return TopicChange; + return Change::Topic; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) emit avatarChanged(); - return AvatarChange; + return Change::Avatar; } , [this,oldStateEvent] (const RoomMemberEvent& evt) { // clang-format on @@ -2767,14 +2765,14 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) case Membership::Undefined: qCWarning(MEMBERS) << "Ignored undefined membership type"; } - return MembersChange; + return Change::Members; // clang-format off } , [this] (const EncryptionEvent&) { // As encryption can only be switched on once, emit the signal here // instead of aggregating and emitting in updateData() emit encryption(); - return OtherChange; + return Change::Other; } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); @@ -2790,18 +2788,18 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return true; }); - return OtherChange; + return Change::Other; // clang-format off } - , OtherChange); + , Change::Other); // clang-format on - Q_ASSERT(result != NoChange); + Q_ASSERT(result != Change::None); return result; } Room::Changes Room::processEphemeralEvent(EventPtr&& event) { - Changes changes = NoChange; + Changes changes {}; QElapsedTimer et; et.start(); if (auto* evt = eventCast(event)) { @@ -2857,10 +2855,10 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) Room::Changes Room::processAccountDataEvent(EventPtr&& event) { - Changes changes = NoChange; + Changes changes {}; if (auto* evt = eventCast(event)) { d->setTags(evt->tags()); - changes |= Change::TagsChange; + changes |= Change::Tags; } if (auto* evt = eventCast(event)) @@ -2879,7 +2877,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) // TODO: Drop AccountDataChange in 0.8 // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around // a statement, not within a statement - QT_IGNORE_DEPRECATIONS(changes |= AccountDataChange | OtherChange;) + QT_IGNORE_DEPRECATIONS(changes |= Change::AccountData | Change::Other;) } return changes; } -- cgit v1.2.3 From d6cf6b32cdd2843c40fc696accd8a6456f1ea15c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 19 Nov 2021 12:45:50 +0100 Subject: Make enum values logging more terse() By default enum class values are logged along with the qualifier; this may or may not be desirable in a given setting. For JoinState(s) and Membership(Mask) operator<< was overloaded to implicitly suppress qualification; however, this is both overly sweeping and uses Qt's internal API for the backend. Instead, a new QDebug manipulator, terse(), is introduced, that does the same as those operator<< overloads but on a per-invocation basis. This makes it slightly more verbose to log enums but makes the QDebug reconfiguration explicit and doesn't require to produce new overloads every time a new enum ends up in logs. And it's built entirely on the published Qt API, reusing the QDebugManip framework that Quotient already has. Also: operator<<(QDebug, QDebugManip) has been moved out of the namespace to fix lookup issues when there's no prior `using namespace Quotient`. --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a376238e..a2ec228a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -461,7 +461,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); - qCDebug(STATE) << "New" << initialJoinState << "Room:" << id; + qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } Room::~Room() { delete d; } @@ -612,7 +612,7 @@ void Room::setJoinState(JoinState state) if (state == oldState) return; d->joinState = state; - qCDebug(STATE) << "Room" << id() << "changed state: " << oldState + qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState << "->" << state; emit joinStateChanged(oldState, state); } -- cgit v1.2.3 From bf5f209d2d237301c65cc0973f1707b9386f3110 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 06:55:09 +0100 Subject: Room: isEventNotable, notificationFor, checkForNotifications Room::isEventNotable has been moved out from Room::Private and made compliant with MSC2654. The concept of Room::checkForNotifications is taken from Quaternion where a method with the same name has been in QuaternionRoom for a long time - however, actual body is a stub for now, always returning { Notification::None } (Quaternion's implementation is too crude to be taken to the library). Now we really need a pushrules processor to fill this method with something reasonably good. Internally the library now calls checkForNotifications() on every event added to the timeline, filling up the events-to-notifications map because it is anticipated that calculation of notifications can be rather resource-intensive and should only be done once for a given event. Finally, Room::notificationsFor is an accessor into the mentioned map, standing next to isEventNotable (but unlike isEventNotable, it's not virtual; checkForNotifications is). --- lib/room.cpp | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a2ec228a..67f65472 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -116,6 +116,7 @@ public: QHash, RelatedEvents> relations; QString displayname; Avatar avatar; + QHash notifications; int highlightCount = 0; int notificationCount = 0; members_map_t membersMap; @@ -241,13 +242,6 @@ public: // return EventT::content_type() // } - bool isEventNotable(const TimelineItem& ti) const - { - return !ti->isRedacted() && ti->senderId() != connection->userId() - && is(*ti) - && ti.viewAs()->replacedEvent().isEmpty(); - } - template Changes updateStateFrom(EventArrayT&& events) { @@ -709,7 +703,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, et.start(); const auto newUnreadMessages = count_if(from, to, - std::bind(&Room::Private::isEventNotable, this, _1)); + std::bind(&Room::isEventNotable, q, _1)); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages in" << q->objectName() << "took" << et; @@ -742,7 +736,7 @@ Room::Changes Room::Private::recalculateUnreadCount(bool force) et.start(); unreadMessages = int(count_if(timeline.crbegin(), q->fullyReadMarker(), - [this](const auto& ti) { return isEventNotable(ti); })); + [this](const auto& ti) { return q->isEventNotable(ti); })); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName() << "took" << et; @@ -837,6 +831,28 @@ bool Room::canSwitchVersions() const return true; } +bool Room::isEventNotable(const TimelineItem &ti) const +{ + const auto& evt = *ti; + const auto* rme = ti.viewAs(); + return !evt.isRedacted() + && (is(evt) || is(evt) + || is(evt) || is(evt) + || (rme && rme->msgtype() != MessageEventType::Notice + && rme->replacedEvent().isEmpty())) + && evt.senderId() != localUser()->id(); +} + +Notification Room::notificationFor(const TimelineItem &ti) const +{ + return d->notifications.value(ti->id()); +} + +Notification Room::checkForNotifications(const TimelineItem &ti) +{ + return { Notification::None }; +} + bool Room::hasUnreadMessages() const { return unreadCount() >= 0; } int Room::unreadCount() const { return d->unreadMessages; } @@ -1548,11 +1564,12 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, !eventsIndex.contains(eId), __FUNCTION__, makeErrorStr(*e, "Event is already in the timeline; " "incoming events were not properly deduplicated")); - if (placement == Older) - timeline.emplace_front(move(e), --index); - else - timeline.emplace_back(move(e), ++index); + const auto& ti = placement == Older + ? timeline.emplace_front(move(e), --index) + : timeline.emplace_back(move(e), ++index); eventsIndex.insert(eId, index); + if (auto n = q->checkForNotifications(ti); n.type != Notification::None) + notifications.insert(e->id(), n); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } const auto insertedSize = (index - baseIndex) * placement; -- cgit v1.2.3 From 96f3daf7a2c4ec875904c11350c93612265e2eed Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 06:03:49 +0100 Subject: SyncData: support MSC2654; partiallyReadCount Since MSC2654's unread count is counted from the m.read receipt, and the course is to follow the spec's terminology and use "unread count" for the number of notable events since m.read, this required to move the existing number of notable events since m.fully_read to another field, henceforth called partiallyReadCount. At the same time, SyncData::notificationCount is dropped completely since MSC2654 claims to supersede it. Also: Room::resetNotificationCount() and Room::resetHighlightCount() are deprecated, as these never worked properly overwriting values that can be calculated or sourced from the server, only for these values to be set back again the next time the room is updated from /sync. --- lib/room.cpp | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 67f65472..bc686962 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -117,7 +117,7 @@ public: QString displayname; Avatar avatar; QHash notifications; - int highlightCount = 0; + qsizetype serverHighlightCount = 0; int notificationCount = 0; members_map_t membersMap; QList usersTyping; @@ -1072,13 +1072,13 @@ void Room::resetNotificationCount() emit notificationCountChanged(); } -int Room::highlightCount() const { return d->highlightCount; } +qsizetype Room::highlightCount() const { return d->serverHighlightCount; } void Room::resetHighlightCount() { - if (d->highlightCount == 0) + if (d->serverHighlightCount == 0) return; - d->highlightCount = 0; + d->serverHighlightCount = 0; emit highlightCountChanged(); } @@ -1673,16 +1673,16 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (merge(d->unreadMessages, data.unreadCount)) { - qCDebug(MESSAGES) << "Loaded unread_count:" << *data.unreadCount // - << "in" << objectName(); + if (merge(d->unreadMessages, data.partiallyReadCount)) { + qCDebug(MESSAGES) << "Loaded partially read count:" + << *data.partiallyReadCount << "in" << objectName(); emit unreadMessagesChanged(this); } - if (merge(d->highlightCount, data.highlightCount)) + if (merge(d->serverHighlightCount, data.highlightCount)) emit highlightCountChanged(); - if (merge(d->notificationCount, data.notificationCount)) + if (merge(d->notificationCount, data.unreadCount)) emit notificationCountChanged(); if (roomChanges) { @@ -3077,16 +3077,15 @@ QJsonObject Room::Private::toJson() const .fullJson() } } }); } - QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, - unreadMessages } }; + QJsonObject unreadNotifObj { { PartiallyReadCountKey, unreadMessages } }; - if (highlightCount > 0) - unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount); - if (notificationCount > 0) - unreadNotifObj.insert(QStringLiteral("notification_count"), - notificationCount); + if (serverHighlightCount > 0) + unreadNotifObj.insert(HighlightCountKey, serverHighlightCount); + + result.insert(UnreadNotificationsKey, unreadNotifObj); - result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); + if (notificationCount > 0) + unreadNotifObj.insert(NewUnreadCountKey, notificationCount); if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took" -- cgit v1.2.3 From e7babe6715672a358f7cc8b90d5df27e21a1b3e8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 07:03:48 +0100 Subject: Room::Private::postprocessChanges() This makes updating display name and emission of necessary signals including Room::changed() more systematic when it has to occur outside of updateData() flow - e.g. when loading all members. --- lib/room.cpp | 85 +++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 32 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index bc686962..1df5dc71 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -267,6 +267,8 @@ public: Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); + void postprocessChanges(Changes changes, bool saveState = true); + /** Move events into the timeline * * Insert events into the timeline, either new or historical. @@ -793,28 +795,36 @@ void Room::setReadReceipt(const QString& atEventId) QUrl::toPercentEncoding(atEventId)); } -void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) +bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - if (upToMarker < q->fullyReadMarker()) { - setFullyReadMarker((*upToMarker)->id()); - // Assuming that if a read receipt was sent on a newer event, it will - // stay there instead of "un-reading" notifications/mentions from - // m.fully_read to m.read + if (setFullyReadMarker(upToMarker->event()->id())) { + // The assumption below is that if a read receipt was sent on a newer + // event, the homeserver will keep it there instead of reverting to + // m.fully_read connection->callApi(BackgroundRequest, id, fullyReadUntilEventId, fullyReadUntilEventId); + return true; } + if (upToMarker != q->historyEdge()) + qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName() + << "is behind the current fully read marker at" + << *q->fullyReadMarker() + << "- won't move fully read marker back in timeline"; + else + qCWarning(MESSAGES) << "Cannot mark an unknown event in" + << q->objectName() << "as fully read"; + return false; } -void Room::markMessagesAsRead(QString uptoEventId) +void Room::markMessagesAsRead(const QString& uptoEventId) { d->markMessagesAsRead(findInTimeline(uptoEventId)); } void Room::markAllMessagesAsRead() { - if (!d->timeline.empty()) - d->markMessagesAsRead(d->timeline.crbegin()); + d->markMessagesAsRead(d->timeline.crbegin()); } bool Room::canSwitchVersions() const @@ -941,8 +951,7 @@ void Room::Private::getAllMembers() it != syncEdge(); ++it) if (is(**it)) roomChanges |= q->processStateEvent(**it); - if (roomChanges & Change::Members) - emit q->memberListChanged(); + postprocessChanges(roomChanges); emit q->allMembersLoaded(); }); } @@ -1459,7 +1468,6 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) return Change::None; qCDebug(STATE).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; - emit q->memberListChanged(); return Change::Summary; } @@ -1651,27 +1659,23 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) setJoinState(data.joinState); Changes roomChanges {}; - for (auto&& event : data.accountData) - roomChanges |= processAccountDataEvent(move(event)); - + // The order of calculation is important - don't merge the lines! roomChanges |= d->updateStateFrom(data.state); - // The order of calculation is important - don't merge these lines! + roomChanges |= d->setSummary(move(data.summary)); roomChanges |= d->addNewMessageEvents(move(data.timeline)); + for (auto&& ephemeralEvent : data.ephemeral) + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); + + for (auto&& event : data.accountData) + roomChanges |= processAccountDataEvent(move(event)); + if (roomChanges & Change::Topic) emit topicChanged(); if (roomChanges & (Change::Name | Change::Aliases)) emit namesChanged(this); - if (roomChanges & Change::Members) - emit memberListChanged(); - - roomChanges |= d->setSummary(move(data.summary)); - - for (auto&& ephemeralEvent : data.ephemeral) - roomChanges |= processEphemeralEvent(move(ephemeralEvent)); - // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (merge(d->unreadMessages, data.partiallyReadCount)) { qCDebug(MESSAGES) << "Loaded partially read count:" @@ -1685,12 +1689,25 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (merge(d->notificationCount, data.unreadCount)) emit notificationCountChanged(); - if (roomChanges) { - d->updateDisplayname(); - emit changed(roomChanges); - if (!fromCache) - connection()->saveRoomState(this); - } + d->postprocessChanges(roomChanges, !fromCache); +} + +void Room::Private::postprocessChanges(Changes changes, bool saveState) +{ + if (!changes) + return; + + if (changes & Change::Members) + emit q->memberListChanged(); + + if (changes + & (Change::Name | Change::Aliases | Change::Members | Change::Summary)) + updateDisplayname(); + + qCDebug(MAIN) << terse << changes << "in" << q->objectName(); + emit q->changed(changes); + if (saveState) + connection->saveRoomState(q); } RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) @@ -2566,6 +2583,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) if (events.empty()) return; + Changes changes {}; // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly @@ -2574,7 +2592,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto& e = *eptr; if (e.isStateEvent() && !currentState.contains({ e.matrixType(), e.stateKey() })) { - q->processStateEvent(e); + changes |= q->processStateEvent(e); } } @@ -2594,7 +2612,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) emit q->updatedEvent(relation.eventId); } } - updateUnreadCount(from, historyEdge()); + changes |= updateUnreadCount(from, historyEdge()); // When there are no unread messages and the read marker is within the // known timeline, unreadMessages == -1 // (see https://github.com/quotient-im/libQuotient/wiki/Developer-notes#2-saving-unread-event-counts). @@ -2604,6 +2622,9 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to" << q->objectName() << "in" << et; + + if (changes) + postprocessChanges(changes); } Room::Changes Room::processStateEvent(const RoomEvent& e) -- cgit v1.2.3 From b2f9b212c78bc9dd7c69f6a2d1f94376adb488e3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 06:55:16 +0100 Subject: EventStats and Room::partiallyRead/unreadStats() This introduces a new API to count unread events that would allow to obtain those unread and highlight counts since either the fully read marker (Room::partiallyReadStats) or the last read receipt (Room::unreadStats). Element uses the read receipt as the anchor to count unread numbers, while Quaternion historically used the fully read marker for that (with the pre-0.7 library sticking the two markers to each other). From now on the meaning of "unread" in Quotient is aligned with that of the spec and Element, and "partially read" means events between the fully read marker and the local read receipt; the design allows client authors to use either or both counting strategies as they see fit. Respectively, Room::P::setFullyReadMarker() updates partially-read statistics, while Room::P::setLastReadReceipt(), when called on a local user, updates unread statistics. Room::notificationCount() and Room::highlightCount() maintain their previous meaning as the counters since the last read receipt; Room::notificationCount() counts unread events locally, falling back to the value from the above-mentioned key defined by MSC2654, and if that is not there, further to `unread_notifications/notification_count` defined in the current spec. Room::highlightCount(), however, is still taken from the homeserver, not from Room::unreadStats().highlightCount. --- lib/room.cpp | 386 +++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 245 insertions(+), 141 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1df5dc71..8bad9084 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -15,6 +15,7 @@ #include "e2ee.h" #include "syncdata.h" #include "user.h" +#include "eventstats.h" // NB: since Qt 6, moc_room.cpp needs User fully defined #include "moc_room.cpp" @@ -118,13 +119,14 @@ public: Avatar avatar; QHash notifications; qsizetype serverHighlightCount = 0; - int notificationCount = 0; + // Starting up with estimate event statistics as there's zero knowledge + // about the timeline. + EventStats partiallyReadStats {}, unreadStats {}; members_map_t membersMap; QList usersTyping; QHash> eventIdReadUsers; QList usersInvited; QList membersLeft; - int unreadMessages = 0; bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; @@ -267,6 +269,7 @@ public: Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); + Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache); void postprocessChanges(Changes changes, bool saveState = true); /** Move events into the timeline @@ -286,12 +289,12 @@ public: */ void dropDuplicateEvents(RoomEvents& events) const; - bool setLastReadReceipt(const QString& userId, rev_iter_t newMarker, - ReadReceipt newReceipt = {}); + Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker, + ReadReceipt newReceipt = {}, + bool deferStatsUpdate = false); Changes setFullyReadMarker(const QString &eventId); - Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); - Changes recalculateUnreadCount(bool force = false); - void markMessagesAsRead(const rev_iter_t &upToMarker); + Changes updateStats(const rev_iter_t& from, const rev_iter_t& to); + bool markMessagesAsRead(const rev_iter_t& upToMarker); void getAllMembers(); @@ -613,9 +616,10 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -bool Room::Private::setLastReadReceipt(const QString& userId, - rev_iter_t newMarker, - ReadReceipt newReceipt) +Room::Changes Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, + ReadReceipt newReceipt, + bool deferStatsUpdate) { if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) newMarker = q->findInTimeline(newReceipt.eventId); @@ -643,10 +647,11 @@ bool Room::Private::setLastReadReceipt(const QString& userId, const auto prevEventId = storedReceipt.eventId; // NB: with reverse iterators, timeline history >= sync edge if (newMarker >= q->findInTimeline(prevEventId)) - return false; + return Change::None; // Finally make the change + Changes changes = Change::Other; auto oldEventReadUsersIt = eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member if (oldEventReadUsersIt != eventIdReadUsers.end()) { @@ -663,21 +668,43 @@ bool Room::Private::setLastReadReceipt(const QString& userId, // for actual members, not just any user const auto member = q->user(userId); Q_ASSERT(member != nullptr); + if (isLocalUser(member) && !deferStatsUpdate) { + if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId), + newMarker)) { + qCDebug(MESSAGES) + << "Updated unread event statistics in" << q->objectName() + << "after moving the local read receipt:" << unreadStats; + changes |= Change::UnreadStats; + } + Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check + } emit q->lastReadEventChanged(member); // TODO: remove in 0.8 if (!isLocalUser(member)) emit q->readMarkerForUserMoved(member, prevEventId, storedReceipt.eventId); - return true; + return changes; } -Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, - const rev_iter_t& to) +Room::Changes Room::Private::updateStats(const rev_iter_t& from, + const rev_iter_t& to) { Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); - auto fullyReadMarker = q->fullyReadMarker(); + const auto fullyReadMarker = q->fullyReadMarker(); + auto readReceiptMarker = q->localReadReceiptMarker(); + Changes changes = Change::None; + // Correct the read receipt to never be behind the fully read marker + if (readReceiptMarker > fullyReadMarker + && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) { + changes |= Change::Other; + readReceiptMarker = q->localReadReceiptMarker(); + qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read " + "marker - it's now corrected to be at index" + << readReceiptMarker->index(); + } + if (fullyReadMarker < from) return Change::None; // What's arrived is already fully read @@ -685,79 +712,70 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, if (fullyReadMarker == historyEdge() && q->allHistoryLoaded()) return setFullyReadMarker(timeline.front()->id()); - // Catch a special case when the last fully read event id refers to an - // event that has just arrived. In this case we should recalculate - // unreadMessages to get an exact number instead of an estimation - // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). - // For the same reason (switching from the estimation to the exact - // number) this branch always emits unreadMessagesChanged() and returns - // UnreadNotifsChange, even if the estimation luckily matched the exact - // result. - if (fullyReadMarker < to) - return recalculateUnreadCount(true); - - // At this point the fully read marker is somewhere beyond the "oldest" - // message from the arrived batch - add up newly arrived messages to - // the current counter, instead of a complete recalculation. - Q_ASSERT(to <= fullyReadMarker); + // Catch a case when the id in the last fully read marker or the local read + // receipt refers to an event that has just arrived. In this case either + // one (unreadStats) or both statistics should be recalculated to get + // an exact number instead of an estimation (see documentation on + // EventStats::isEstimate). For the same reason (switching from the + // estimate to the exact number) this branch forces returning + // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if + // the estimation luckily matched the exact result. + if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) { + unreadStats = EventStats::fromMarker(q, readReceiptMarker); + Q_ASSERT(!unreadStats.isEstimate); + qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in" + << q->objectName() << ": " << unreadStats; + changes |= Change::UnreadStats; + if (fullyReadMarker < to) { + // Add up to unreadStats instead of counting same events again + partiallyReadStats = EventStats::fromRange(q, readReceiptMarker, + q->fullyReadMarker(), + unreadStats); + Q_ASSERT(!partiallyReadStats.isEstimate); + + qCDebug(MESSAGES).nospace() + << "Recalculated partially read event statistics in " + << q->objectName() << ": " << partiallyReadStats; + return Change::PartiallyReadStats | Change::UnreadStats; + } + } - QElapsedTimer et; - et.start(); - const auto newUnreadMessages = - count_if(from, to, - std::bind(&Room::isEventNotable, q, _1)); - if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Counting gained unread messages in" - << q->objectName() << "took" << et; - - if (newUnreadMessages == 0) - return Change::None; + // As of here, at least the fully read marker (but maybe also read receipt) + // points to somewhere beyond the "oldest" message from the arrived batch - + // add up newly arrived messages to the current stats, instead of a complete + // recalculation. + Q_ASSERT(fullyReadMarker >= to); - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages < 0) - unreadMessages = 0; + const auto newStats = EventStats::fromRange(q, from, to); + Q_ASSERT(!newStats.isEstimate); + if (newStats.notableCount == 0 || newStats.highlightCount == 0) + return changes; - unreadMessages += newUnreadMessages; - qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->fullyReadMarker() == timeline.crend() - ? "in total at least" - : "in total") - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - return Change::UnreadNotifs; -} + const auto doAddStats = [this, &changes, newStats](EventStats& s, + const rev_iter_t& marker, + Change c) { + s.notableCount += newStats.notableCount; + s.highlightCount += newStats.highlightCount; + if (!s.isEstimate) + s.isEstimate = marker == historyEdge(); + changes |= c; + }; -Room::Changes Room::Private::recalculateUnreadCount(bool force) -{ - // The recalculation logic assumes that the fully read marker points at - // a specific position in the timeline - Q_ASSERT(q->fullyReadMarker() != timeline.crend()); - const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; - et.start(); - unreadMessages = - int(count_if(timeline.crbegin(), q->fullyReadMarker(), - [this](const auto& ti) { return q->isEventNotable(ti); })); - if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName() - << "took" << et; - - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; - - if (!force && unreadMessages == oldUnreadCount) - return Change::None; + doAddStats(partiallyReadStats, fullyReadMarker, Change::PartiallyReadStats); + if (readReceiptMarker >= to) { + // readReceiptMarker < to branch shouldn't have been entered + Q_ASSERT(!changes.testFlag(Change::UnreadStats)); + doAddStats(unreadStats, readReceiptMarker, Change::UnreadStats); + } + qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newStats + << "notable/highlighted event(s); total statistics:" + << partiallyReadStats << "since the fully read marker," + << unreadStats << "since read receipt"; - if (unreadMessages == -1) - qCDebug(MESSAGES) - << "Room" << displayname << "has no more unread messages"; - else - qCDebug(MESSAGES) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - return Change::UnreadNotifs; + // Check invariants + Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker)); + Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker)); + return changes; } Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) @@ -765,45 +783,59 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) if (fullyReadUntilEventId == eventId) return Change::None; + const auto prevReadMarker = q->fullyReadMarker(); + const auto newReadMarker = q->findInTimeline(eventId); + if (newReadMarker > prevReadMarker) + return Change::None; + const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // << "set to" << fullyReadUntilEventId; - emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - // TODO: Remove in 0.8 - emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker;) + QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;) if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { - // Pull read receipt if it's behind - setLastReadReceipt(connection->userId(), rm); - changes |= recalculateUnreadCount(); // TODO: updateUnreadCount()? + // Pull read receipt if it's behind, and update statistics + changes |= setLastReadReceipt(connection->userId(), rm); + if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) { + changes |= Change::PartiallyReadStats; + qCDebug(MESSAGES) + << "Updated partially read event statistics in" + << q->objectName() + << "after moving m.fully_read marker: " << partiallyReadStats; + } + Q_ASSERT(partiallyReadStats.isValidFor(q, rm)); // post-check } + emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); + // TODO: Remove in 0.8 + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); return changes; } void Room::setReadReceipt(const QString& atEventId) { - if (!d->setLastReadReceipt(localUser()->id(), historyEdge(), - { atEventId, QDateTime::currentDateTime() })) { + if (const auto changes = d->setLastReadReceipt(localUser()->id(), + historyEdge(), + { atEventId })) { + connection()->callApi(BackgroundRequest, id(), + QStringLiteral("m.read"), + QUrl::toPercentEncoding(atEventId)); + d->postprocessChanges(changes); + } else qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id() << "in" << objectName() << "is at or behind the old one, skipping"; - return; - } - connection()->callApi(BackgroundRequest, id(), - QStringLiteral("m.read"), - QUrl::toPercentEncoding(atEventId)); } bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - if (setFullyReadMarker(upToMarker->event()->id())) { + if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) { // The assumption below is that if a read receipt was sent on a newer // event, the homeserver will keep it there instead of reverting to // m.fully_read connection->callApi(BackgroundRequest, id, fullyReadUntilEventId, fullyReadUntilEventId); + postprocessChanges(changes); return true; } if (upToMarker != q->historyEdge()) @@ -863,9 +895,18 @@ Notification Room::checkForNotifications(const TimelineItem &ti) return { Notification::None }; } -bool Room::hasUnreadMessages() const { return unreadCount() >= 0; } +bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); } -int Room::unreadCount() const { return d->unreadMessages; } +int countFromStats(const EventStats& s) +{ + return s.empty() ? -1 : int(s.notableCount); +} + +int Room::unreadCount() const { return countFromStats(partiallyReadStats()); } + +EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; } + +EventStats Room::unreadStats() const { return d->unreadStats; } Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); } @@ -1071,13 +1112,16 @@ QSet Room::usersAtEventId(const QString& eventId) return users; } -int Room::notificationCount() const { return d->notificationCount; } +qsizetype Room::notificationCount() const +{ + return d->unreadStats.notableCount; +} void Room::resetNotificationCount() { - if (d->notificationCount == 0) + if (d->unreadStats.notableCount == 0) return; - d->notificationCount = 0; + d->unreadStats.notableCount = 0; emit notificationCountChanged(); } @@ -1652,6 +1696,72 @@ QUrl Room::memberAvatarUrl(const QString &mxId) const : QUrl(); } +Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, + bool fromCache) +{ + Changes changes {}; + if (fromCache) { + // Initial load of cached statistics + partiallyReadStats = + EventStats::fromCachedCounters(data.partiallyReadCount); + unreadStats = EventStats::fromCachedCounters(data.unreadCount, + data.highlightCount); + // Migrate from lib 0.6: -1 in the old unread counter overrides 0 + // (which loads to an estimate) in notification_count. Next caching will + // save -1 in both places, completing the migration. + if (data.unreadCount == 0 && data.partiallyReadCount == -1) + unreadStats.isEstimate = false; + changes |= Change::PartiallyReadStats | Change::UnreadStats; + qCDebug(MESSAGES) << "Loaded" << q->objectName() + << "event statistics from cache:" << partiallyReadStats + << "since m.fully_read," << unreadStats + << "since m.read"; + } else if (timeline.empty()) { + // In absence of actual events use statistics from the homeserver + if (merge(unreadStats.notableCount, data.unreadCount)) + changes |= Change::PartiallyReadStats; + if (merge(unreadStats.highlightCount, data.highlightCount)) + changes |= Change::UnreadStats; + unreadStats.isEstimate = !data.unreadCount.has_value() + || *data.unreadCount > 0; + qCDebug(MESSAGES) + << "Using server-side unread event statistics while the" + << q->objectName() << "timeline is empty:" << unreadStats; + } + bool correctedStats = false; + if (unreadStats.highlightCount > partiallyReadStats.highlightCount) { + correctedStats = true; + partiallyReadStats.highlightCount = unreadStats.highlightCount; + partiallyReadStats.isEstimate |= unreadStats.isEstimate; + } + if (unreadStats.notableCount > partiallyReadStats.notableCount) { + correctedStats = true; + partiallyReadStats.notableCount = unreadStats.notableCount; + partiallyReadStats.isEstimate |= unreadStats.isEstimate; + } + if (!unreadStats.isEstimate && partiallyReadStats.isEstimate) { + correctedStats = true; + partiallyReadStats.isEstimate = true; + } + if (correctedStats) + qCDebug(MESSAGES) << "Partially read event statistics in" + << q->objectName() << "were adjusted to" + << partiallyReadStats + << "to be consistent with the m.read receipt"; + Q_ASSERT(partiallyReadStats.isValidFor(q, q->fullyReadMarker())); + Q_ASSERT(unreadStats.isValidFor(q, q->localReadReceiptMarker())); + + // TODO: Once the library learns to count highlights, drop + // serverHighlightCount and only use the server-side counter when + // the timeline is empty (see the code above). + if (merge(serverHighlightCount, data.highlightCount)) { + qCDebug(MESSAGES) << "Updated highlights number in" << q->objectName() + << "to" << serverHighlightCount; + changes |= Change::Highlights; + } + return changes; +} + void Room::updateData(SyncRoomData&& data, bool fromCache) { if (d->prevBatch.isEmpty()) @@ -1670,25 +1780,14 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) for (auto&& event : data.accountData) roomChanges |= processAccountDataEvent(move(event)); + roomChanges |= d->updateStatsFromSyncData(data, fromCache); + if (roomChanges & Change::Topic) emit topicChanged(); if (roomChanges & (Change::Name | Change::Aliases)) emit namesChanged(this); - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (merge(d->unreadMessages, data.partiallyReadCount)) { - qCDebug(MESSAGES) << "Loaded partially read count:" - << *data.partiallyReadCount << "in" << objectName(); - emit unreadMessagesChanged(this); - } - - if (merge(d->serverHighlightCount, data.highlightCount)) - emit highlightCountChanged(); - - if (merge(d->notificationCount, data.unreadCount)) - emit notificationCountChanged(); - d->postprocessChanges(roomChanges, !fromCache); } @@ -1704,6 +1803,17 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) & (Change::Name | Change::Aliases | Change::Members | Change::Summary)) updateDisplayname(); + if (changes & Change::PartiallyReadStats) { + emit q->unreadMessagesChanged(q); // TODO: remove in 0.8 + emit q->partiallyReadStatsChanged(); + } + + if (changes & Change::UnreadStats) + emit q->unreadStatsChanged(); + + if (changes & Change::Highlights) + emit q->highlightCountChanged(); + qCDebug(MAIN) << terse << changes << "in" << q->objectName(); emit q->changed(changes); if (saveState) @@ -2553,17 +2663,16 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << totalInserted << "new events; the last event is now" << timeline.back(); - const auto& firstWriterId = (*from)->senderId(); - setLastReadReceipt(firstWriterId, rev_iter_t(from + 1)); + roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from)); + // If the local user's message(s) is/are first in the batch // and the fully read marker was right before it, promote // the fully read marker to the same event as the read receipt. + const auto& firstWriterId = (*from)->senderId(); if (firstWriterId == connection->userId() - && q->fullyReadMarker().base() == from) // + && q->fullyReadMarker().base() == from) roomChanges |= setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId); - - roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } Q_ASSERT(timeline.size() == timelineSize + totalInserted); @@ -2612,17 +2721,12 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) emit q->updatedEvent(relation.eventId); } } - changes |= updateUnreadCount(from, historyEdge()); - // When there are no unread messages and the read marker is within the - // known timeline, unreadMessages == -1 - // (see https://github.com/quotient-im/libQuotient/wiki/Developer-notes#2-saving-unread-event-counts). - Q_ASSERT(unreadMessages != 0 || q->fullyReadMarker() == historyEdge()); - Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to" << q->objectName() << "in" << et; + changes |= updateStats(from, historyEdge()); if (changes) postprocessChanges(changes); } @@ -2860,8 +2964,9 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) totalReceipts += p.receipts.size(); const auto newMarker = findInTimeline(p.evtId); if (newMarker == historyEdge()) - qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " - "found; saving anyway"; + qCDebug(EPHEMERAL) + << "Event" << p.evtId + << "is not found; saving read receipt(s) anyway"; // If the event is not found (most likely, because it's too old and // hasn't been fetched from the server yet) but there is a previous // marker for a user, keep the previous marker because read receipts @@ -2870,9 +2975,12 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) // the event is fetched later on. const auto updatedCount = std::count_if( p.receipts.cbegin(), p.receipts.cend(), - [this, &newMarker, &evtId = p.evtId](const auto& r) -> bool { - return d->setLastReadReceipt(r.userId, newMarker, - { evtId, r.timestamp }); + [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) { + const auto change = + d->setLastReadReceipt(r.userId, newMarker, + { evtId, r.timestamp }); + changes |= change; + return change.testFlag(Change::Any); }); if (p.receipts.size() > 1) @@ -3098,15 +3206,11 @@ QJsonObject Room::Private::toJson() const .fullJson() } } }); } - QJsonObject unreadNotifObj { { PartiallyReadCountKey, unreadMessages } }; - - if (serverHighlightCount > 0) - unreadNotifObj.insert(HighlightCountKey, serverHighlightCount); - - result.insert(UnreadNotificationsKey, unreadNotifObj); - - if (notificationCount > 0) - unreadNotifObj.insert(NewUnreadCountKey, notificationCount); + result.insert(UnreadNotificationsKey, + QJsonObject { { PartiallyReadCountKey, + countFromStats(partiallyReadStats) }, + { HighlightCountKey, serverHighlightCount } }); + result.insert(NewUnreadCountKey, countFromStats(unreadStats)); if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took" -- cgit v1.2.3 From ffb1f978c377e0442bed9eab17727932d59fc55a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 12:24:11 +0100 Subject: Log the hexadecimal value of Changes too, just in case --- lib/room.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 8bad9084..c166254e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1814,7 +1814,11 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) if (changes & Change::Highlights) emit q->highlightCountChanged(); - qCDebug(MAIN) << terse << changes << "in" << q->objectName(); + qCDebug(MAIN) << terse << changes << "= hex" << +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + Qt:: +#endif + hex << uint(changes) << "in" << q->objectName(); emit q->changed(changes); if (saveState) connection->saveRoomState(q); -- cgit v1.2.3 From 59ab0d5e04152e9657bdb47a6c9e860f4864be20 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 22 Nov 2021 17:58:54 +0100 Subject: Fix a crash on trying to mark unknown events as fully read --- lib/room.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c166254e..e8d9b1bf 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -828,7 +828,10 @@ void Room::setReadReceipt(const QString& atEventId) bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) { + if (upToMarker == q->historyEdge()) + qCWarning(MESSAGES) << "Cannot mark an unknown event in" + << q->objectName() << "as fully read"; + else if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) { // The assumption below is that if a read receipt was sent on a newer // event, the homeserver will keep it there instead of reverting to // m.fully_read @@ -837,15 +840,11 @@ bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) fullyReadUntilEventId); postprocessChanges(changes); return true; - } - if (upToMarker != q->historyEdge()) + } else qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName() << "is behind the current fully read marker at" << *q->fullyReadMarker() << "- won't move fully read marker back in timeline"; - else - qCWarning(MESSAGES) << "Cannot mark an unknown event in" - << q->objectName() << "as fully read"; return false; } -- cgit v1.2.3 From 2fdcb290c7c5249896e3f8b542df16e248487f34 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 22 Nov 2021 22:36:11 +0100 Subject: Fix stupid false-negatives from Room::updateStats() --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e8d9b1bf..3090cb7b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -736,7 +736,7 @@ Room::Changes Room::Private::updateStats(const rev_iter_t& from, qCDebug(MESSAGES).nospace() << "Recalculated partially read event statistics in " << q->objectName() << ": " << partiallyReadStats; - return Change::PartiallyReadStats | Change::UnreadStats; + return changes | Change::PartiallyReadStats; } } @@ -748,7 +748,7 @@ Room::Changes Room::Private::updateStats(const rev_iter_t& from, const auto newStats = EventStats::fromRange(q, from, to); Q_ASSERT(!newStats.isEstimate); - if (newStats.notableCount == 0 || newStats.highlightCount == 0) + if (newStats.empty()) return changes; const auto doAddStats = [this, &changes, newStats](EventStats& s, -- cgit v1.2.3 From b3d62050befbb1c526f03e4356f3263d197c45f2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 26 Nov 2021 14:02:49 +0100 Subject: Event: deprecate originalJson[Object]() The "original JSON" wording is misleading: the returned JSON can be and is routinely edited as a part of event construction, redaction, editing. Also, originalJson() name is misleading in that it returns a stringified (in a very specific way) JSON and not an object. You have to call fullJson() to get the object, and originalJsonObject(), confusingly, returns exactly the same thing but as a value rather than as a reference. The original intention of keeping originalJsonObject() was to make it Q_INVOKABLE or use it as an accessor for a Q_PROPERTY. unfortunately, this was never really practical as discussed in the previous commit. All that implies that clients have to handle passing event JSON to QML themselves, in the form they prefer (as an object or a string). The added complexity is negligible though; on the other hand, there's added flexibility in, e.g., choosing a compact instead of default JSON layout or even generate a highlighted JSON representation. --- lib/room.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 3090cb7b..aa00025c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1589,7 +1589,8 @@ void Room::Private::removeMemberFromMap(User* u) inline auto makeErrorStr(const Event& e, QByteArray msg) { - return msg.append("; event dump follows:\n").append(e.originalJson()); + return msg.append("; event dump follows:\n") + .append(QJsonDocument(e.fullJson()).toJson()); } Room::Timeline::size_type @@ -2363,7 +2364,7 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { - auto originalJson = target.originalJsonObject(); + auto originalJson = target.fullJson(); // clang-format off static const QStringList keepKeys { EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey, @@ -2409,7 +2410,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, originalJson.insert(ContentKey, content); } auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); - unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject(); + unsignedData[RedactedCauseKeyL] = redaction.fullJson(); originalJson.insert(QStringLiteral("unsigned"), unsignedData); return loadEvent(originalJson); @@ -2479,7 +2480,7 @@ RoomEventPtr makeReplaced(const RoomEvent& target, if (!targetReply.empty()) { newContent["m.relates_to"] = targetReply; } - auto originalJson = target.originalJsonObject(); + auto originalJson = target.fullJson(); originalJson[ContentKeyL] = newContent; auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); -- cgit v1.2.3 From 429b3adaf921dd7534a04fccae1bc3c1801d2e40 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 26 Nov 2021 17:27:23 +0100 Subject: Room::processEphemeralEvents: Fix updatedCount always being zero Trying to test bits with Changes::testFlag(Change::Any) was a bad idea. Along the way: made logging in setLastReadReceipt() refer to the actual timeline item when possible. --- lib/room.cpp | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index aa00025c..68095412 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -632,9 +632,9 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, }); // eagerMarker is now just after the desired event for newMarker if (eagerMarker != newMarker.base()) { - qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId - << "to" << newReceipt.eventId; newMarker = rev_iter_t(eagerMarker); + qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId + << "to" << *newMarker; } // Fill newReceipt with the event (and, if needed, timestamp) from // eagerMarker @@ -645,8 +645,11 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, auto& storedReceipt = lastReadReceipts[userId]; // clazy:exclude=detaching-member const auto prevEventId = storedReceipt.eventId; - // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->findInTimeline(prevEventId)) + // Check that either the new marker is actually "newer" than the current one + // or, if both markers are at historyEdge(), event ids are different. + // NB: with reverse iterators, timeline history edge >= sync edge + if (prevEventId == newReceipt.eventId + || newMarker > q->findInTimeline(prevEventId)) return Change::None; // Finally make the change @@ -661,8 +664,15 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, } eventIdReadUsers[newReceipt.eventId].insert(userId); storedReceipt = move(newReceipt); - qCDebug(EPHEMERAL) << "The new read receipt for" << userId << "is now at" - << storedReceipt.eventId; + + { + auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug + dbg << "The new read receipt for" << userId << "is now at"; + if (newMarker == historyEdge()) + dbg << storedReceipt.eventId; + else + dbg << *newMarker; + } // TODO: use Room::member() when it becomes a thing and only emit signals // for actual members, not just any user @@ -2984,7 +2994,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) d->setLastReadReceipt(r.userId, newMarker, { evtId, r.timestamp }); changes |= change; - return change.testFlag(Change::Any); + return change & Change::Any; }); if (p.receipts.size() > 1) -- cgit v1.2.3 From 38eaa61b9fa8e0a04baa847824bb1e1280395a57 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 26 Nov 2021 17:33:46 +0100 Subject: Fix crashing on invalid member and encryption events The problem is in Room::processStateEvent(): after potentially-inserting-nullptr into currentState, pre-check failure (that may occur on member and trigger events for now) leaves that nullptr in the hash map. Basically anything that uses currentState (e.g., Room::toJson) assumes that currentState has no nullptrs - which leads to either an assertion failure, or nullptr dereferencing. The fix removes the nullptr placeholder if the pre-checks failed. --- lib/room.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 68095412..284eacd1 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2835,8 +2835,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) } , true); // By default, go forward with the state change // clang-format on - if (!proceed) + if (!proceed) { + if (!curStateEvent) // Remove the empty placeholder if one was created + d->currentState.remove({ e.matrixType(), e.stateKey() }); return Change::None; + } // Change the state const auto* const oldStateEvent = -- cgit v1.2.3 From 9701f24ac0214e4d8bd7fe1d6d4fa542d75d3d18 Mon Sep 17 00:00:00 2001 From: Smitty Date: Fri, 26 Nov 2021 20:25:58 -0500 Subject: Add Room::{stateEventsOfType,currentState} This is useful for implementing Spaces support, where all events of type `m.space.child` are needed, and we don't know their state keys in advance. --- lib/room.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 3b00d2d9..7e0806a7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -224,9 +224,15 @@ public: return evt; } - const QHash stateEvents() const + QVector stateEventsOfType(const QString& evtType) const { - return currentState; + QVector vals = QVector(); + for (const auto* val : currentState) { + if (val->matrixType() == evtType) { + vals.append(val); + } + } + return vals; } template @@ -1298,9 +1304,15 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } -const QHash Room::stateEvents() const +const QVector +Room::stateEventsOfType(const QString& evtType) const +{ + return d->stateEventsOfType(evtType); +} + +const QHash& Room::currentState() const { - return d->stateEvents(); + return d->currentState; } RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) -- cgit v1.2.3 From 0425fd280e7eee7a4c9bcf18f79910f181322c42 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 27 Nov 2021 20:14:53 +0100 Subject: Event::content() -> contentPart() There's a clash between Event::content() (a template function) and RoomMessageEvent::content() (plain member). Out of these two, the name more fits to the RME's member function - strictly speaking, Event::content() retrieves a part of content, and so is renamed. In addition, contentPart() defaults to QJsonValue now, which is pretty intuitive (the function returns values from a JSON object) and allows to implement more elaborate logic such as if (const auto v = contentPart<>("key"_ls); v.isObject()) { // foo } else if (v.isString()) { // bar } else { // boo } --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index fac24e5e..8f430ccd 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2507,8 +2507,8 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) RoomEventPtr makeReplaced(const RoomEvent& target, const RoomMessageEvent& replacement) { - const auto &targetReply = target.contentJson()["m.relates_to"].toObject(); - auto newContent = replacement.contentJson().value("m.new_content"_ls).toObject(); + const auto& targetReply = target.contentPart("m.relates_to"); + auto newContent = replacement.contentPart("m.new_content"_ls); if (!targetReply.empty()) { newContent["m.relates_to"] = targetReply; } -- cgit v1.2.3 From 102ed8b661e6ca66d754de0d102713d930fb224d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 27 Nov 2021 22:29:11 +0100 Subject: Code cleanup --- lib/room.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 8f430ccd..6e6d7f11 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -228,12 +228,11 @@ public: QVector stateEventsOfType(const QString& evtType) const { - QVector vals = QVector(); - for (const auto* val : currentState) { - if (val->matrixType() == evtType) { - vals.append(val); - } - } + auto vals = QVector(); + for (auto it = currentState.cbegin(); it != currentState.cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + return vals; } -- cgit v1.2.3 From d7f96a88710235c7c8648a7216bb5ad7d779889a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 29 Nov 2021 10:58:07 +0100 Subject: Track room stubbed state size in logs --- lib/room.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6e6d7f11..4d60570d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -217,6 +217,7 @@ public: evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; + qCDebug(STATE) << "Stubbed state size:" << stubbedState.size(); } evt = stubbedState[evtKey].get(); Q_ASSERT(evt); -- cgit v1.2.3 From 9f71b2a79fba7c5d5ce09ebfdd482c8c470203d9 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 28 Jan 2021 21:59:20 +0100 Subject: Remove duplicated file --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6e6d7f11..0c9af2b9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,7 +12,7 @@ #include "avatar.h" #include "connection.h" #include "converters.h" -#include "e2ee.h" +#include "crypto/e2ee.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" -- cgit v1.2.3 From d72f220e3e3a3b243fdafd93d1405f8207dc516a Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Thu, 28 Jan 2021 23:51:56 +0300 Subject: E2EE: initial port to internal olm wrapper Remove qtolm git module. Update CMakeLists.txt. Rename olm to crypto subdir to prevent disambiguation. Rename internal files accordingly. Comment out not ported E2EE API usage. --- lib/room.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 0c9af2b9..d86b2813 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -65,13 +65,12 @@ #include #ifdef Quotient_E2EE_ENABLED -#include // QtOlm -#include // QtOlm -#include // QtOlm +# include "crypto/qolmaccount.h" +# include "crypto/qolmerrors.h" +# include "crypto/qolminboundsession.h" #endif // Quotient_E2EE_ENABLED using namespace Quotient; -using namespace QtOlm; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -370,23 +369,25 @@ public: // A map from senderKey to a map of sessionId to InboundGroupSession // Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. - QHash, InboundGroupSession*> groupSessions; // TODO: + QHash, QOlmInboundGroupSession*> groupSessions; // TODO: // cache bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) { + // new e2ee TODO: + /* if (groupSessions.contains({ senderKey, sessionId })) { qCDebug(E2EE) << "Inbound Megolm session" << sessionId << "with senderKey" << senderKey << "already exists"; return false; } - InboundGroupSession* megolmSession; + QOlmInboundGroupSession* megolmSession; try { - megolmSession = new InboundGroupSession(sessionKey.toLatin1(), + megolmSession = new QOlmInboundGroupSession(sessionKey.toLatin1(), InboundGroupSession::Init, q); - } catch (OlmError* e) { + } catch (QOlmError* e) { qCDebug(E2EE) << "Unable to create new InboundGroupSession" << e->what(); return false; @@ -398,6 +399,7 @@ public: return false; } groupSessions.insert({ senderKey, sessionId }, megolmSession); + */ return true; } @@ -408,6 +410,8 @@ public: QDateTime timestamp) { std::pair decrypted; + // new e2ee TODO: + /* QPair senderSessionPairKey = qMakePair(senderKey, sessionId); if (!groupSessions.contains(senderSessionPairKey)) { @@ -416,7 +420,7 @@ public: "this message"; return QString(); } - InboundGroupSession* senderSession = + QOlmInboundGroupSession* senderSession = groupSessions.value(senderSessionPairKey); if (!senderSession) { qCDebug(E2EE) << "Unable to decrypt event" << eventId @@ -425,7 +429,7 @@ public: } try { decrypted = senderSession->decrypt(cipher); - } catch (OlmError* e) { + } catch (QOlmError* e) { qCDebug(E2EE) << "Unable to decrypt event" << eventId << "with matching megolm session:" << e->what(); return QString(); @@ -443,6 +447,7 @@ public: return QString(); } } + */ return decrypted.first; } -- cgit v1.2.3 From e9527012622497b0c418df9442180df58401d394 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 15 Feb 2021 18:03:33 +0100 Subject: Apply suggestions from code review Co-authored-by: Nicolas Fella <6377822+nicolasfella@users.noreply.github.com> --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index d86b2813..a8a7fe0c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -65,7 +65,7 @@ #include #ifdef Quotient_E2EE_ENABLED -# include "crypto/qolmaccount.h" +#include "crypto/qolmaccount.h" # include "crypto/qolmerrors.h" # include "crypto/qolminboundsession.h" #endif // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 97f2d162618e7fb2473c184c77875ac9d5e8d1d5 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 15 Feb 2021 18:10:34 +0100 Subject: Apply a few more comments --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a8a7fe0c..1a7a9911 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -66,8 +66,8 @@ #ifdef Quotient_E2EE_ENABLED #include "crypto/qolmaccount.h" -# include "crypto/qolmerrors.h" -# include "crypto/qolminboundsession.h" +#include "crypto/qolmerrors.h" +#include "crypto/qolminboundsession.h" #endif // Quotient_E2EE_ENABLED using namespace Quotient; -- cgit v1.2.3 From c408b460bea2010c6745e03c549e136d6b1d9ec6 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 19 May 2021 00:35:53 +0200 Subject: Start tracking user's devices when a a room starts being encrypted --- lib/room.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1a7a9911..b6022f1b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -475,6 +475,9 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); + connectSingleShot(this, &Room::encryption, this, [=](){ + connection->newEncryptedRoom(this); + }); qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } -- cgit v1.2.3 From f451813f21a76e8c011bbd27f4ded1d31044a572 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 19 May 2021 22:25:50 +0200 Subject: Update tracked users list when new user joins encrypted room --- lib/room.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index b6022f1b..2707842c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -476,7 +476,12 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) return this == r; // loadedRoomState fires only once per room }); connectSingleShot(this, &Room::encryption, this, [=](){ - connection->newEncryptedRoom(this); + connection->encryptionUpdate(this); + }); + connect(this, &Room::userAdded, this, [=](){ + if(usesEncryption()) { + connection->encryptionUpdate(this); + } }); qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } -- cgit v1.2.3 From 0ec4df82265c2f796035c0c103b9f6693f62e24a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 10 Jun 2021 23:57:15 +0200 Subject: Fix setting encrypted flag in rooms --- lib/room.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 2707842c..3a894b9b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1913,7 +1913,6 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) return; } it->setDeparted(); - qCDebug(EVENTS) << "Event txn" << txnId << "has departed"; emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); Room::connect(call, &BaseJob::failure, q, -- cgit v1.2.3 From a30d457161fcaadfe944e4411d4b0e487e856178 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 00:48:23 +0200 Subject: Fix build without E2EE --- lib/room.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 3a894b9b..57914db4 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -475,6 +475,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); +#ifdef Quotient_E2EE_ENABLED connectSingleShot(this, &Room::encryption, this, [=](){ connection->encryptionUpdate(this); }); @@ -483,6 +484,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connection->encryptionUpdate(this); } }); +#endif qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } -- cgit v1.2.3 From 703b3f89ef54d9d40c9117788d0920b6b745bd62 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 12 Jun 2021 23:04:17 +0200 Subject: Implement (meg)olm key caching, megolm decrypting, EncryptedEvent decryption, handling of encrypted redactions and replies --- lib/room.cpp | 195 ++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 141 insertions(+), 54 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 57914db4..5fedd861 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -369,37 +369,95 @@ public: // A map from senderKey to a map of sessionId to InboundGroupSession // Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. - QHash, QOlmInboundGroupSession*> groupSessions; // TODO: - // cache + std::map, std::unique_ptr> groupSessions; + + void loadMegOlmSessions() { + QFile file { connection->stateCacheDir().filePath("megolmsessions.json") }; + if(!file.exists() || !file.open(QIODevice::ReadOnly)) { + qCDebug(E2EE) << "No megolm sessions cache exists."; + return; + } + auto data = file.readAll(); + const auto json = data.startsWith('{') + ? QJsonDocument::fromJson(data).object() +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + : QCborValue::fromCbor(data).toJsonValue().toObject() +#else + : QJsonDocument::fromBinaryData(data).object() +#endif + ; + if (json.isEmpty()) { + qCWarning(MAIN) << "Megolm sessions cache is empty"; + return; + } + for(const auto &s : json["sessions"].toArray()) { + auto pickle = s.toObject()["pickle"].toString().toLatin1(); + auto senderKey = s.toObject()["sender_key"].toString(); + auto sessionId = s.toObject()["session_id"].toString(); + auto sessionResult = QOlmInboundGroupSession::unpickle(pickle, Unencrypted{}); + if(std::holds_alternative(sessionResult)) { + qCWarning(E2EE) << "Failed to unpickle olm session"; + continue; + } + groupSessions[qMakePair(senderKey, sessionId)] = std::move(std::get>(sessionResult)); + } + } + void saveMegOlmSessions() { + QFile outFile { connection->stateCacheDir().filePath("megolmsessions.json") }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); + qCWarning(E2EE) << "Failed to write megolm sessions"; + return; + } + + QJsonObject rootObj { + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), 1 }, + { QStringLiteral("minor"), 0 } } } + }; + { + QJsonArray sessionsJson; + for (const auto &session : groupSessions) { + auto pickleResult = session.second->pickle(Unencrypted{}); + sessionsJson += QJsonObject { + {QStringLiteral("sender_key"), session.first.first}, + {QStringLiteral("session_id"), session.first.second}, + {QStringLiteral("pickle"), QString(pickleResult)} + }; + } + rootObj.insert(QStringLiteral("sessions"), sessionsJson); + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); +#else + QJsonDocument json { rootObj }; + const auto data = json.toJson(QJsonDocument::Compact); +#endif + + outFile.write(data.data(), data.size()); + qCDebug(E2EE) << "Megolm sessions saved to" << outFile.fileName(); + } bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) { - // new e2ee TODO: - /* - if (groupSessions.contains({ senderKey, sessionId })) { - qCDebug(E2EE) << "Inbound Megolm session" << sessionId + if (groupSessions.find(qMakePair(senderKey, sessionId)) != groupSessions.end()) { + qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "with senderKey" << senderKey << "already exists"; return false; } - QOlmInboundGroupSession* megolmSession; - try { - megolmSession = new QOlmInboundGroupSession(sessionKey.toLatin1(), - InboundGroupSession::Init, - q); - } catch (QOlmError* e) { - qCDebug(E2EE) << "Unable to create new InboundGroupSession" - << e->what(); + std::unique_ptr megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); + if (megolmSession->sessionId() != sessionId) { + qCWarning(E2EE) << "Session ID mismatch in m.room_key event sent " + "from sender with key" << senderKey; return false; } - if (megolmSession->id() != sessionId) { - qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent " - "from sender with key" - << senderKey; - return false; - } - groupSessions.insert({ senderKey, sessionId }, megolmSession); - */ + qCWarning(E2EE) << "Adding inbound session"; + groupSessions[qMakePair(senderKey, sessionId)] = std::move(megolmSession); + saveMegOlmSessions(); return true; } @@ -409,46 +467,33 @@ public: const QString& eventId, QDateTime timestamp) { - std::pair decrypted; - // new e2ee TODO: - /* QPair senderSessionPairKey = qMakePair(senderKey, sessionId); - if (!groupSessions.contains(senderSessionPairKey)) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId + if (groupSessions.find(senderSessionPairKey) == groupSessions.end()) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId << "The sender's device has not sent us the keys for " "this message"; return QString(); } - QOlmInboundGroupSession* senderSession = - groupSessions.value(senderSessionPairKey); - if (!senderSession) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "senderSessionPairKey:" << senderSessionPairKey; + auto& senderSession = groupSessions[senderSessionPairKey]; + auto decryptResult = senderSession->decrypt(cipher); + if(std::holds_alternative(decryptResult)) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << std::get(decryptResult); return QString(); } - try { - decrypted = senderSession->decrypt(cipher); - } catch (QOlmError* e) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << e->what(); - return QString(); - } - QPair properties = groupSessionIndexRecord.value( - qMakePair(senderSession->id(), decrypted.second)); + std::pair decrypted = std::get>(decryptResult); + QPair properties = groupSessionIndexRecord.value(qMakePair(senderSession->sessionId(), decrypted.second)); if (properties.first.isEmpty()) { - groupSessionIndexRecord.insert(qMakePair(senderSession->id(), - decrypted.second), - qMakePair(eventId, timestamp)); + groupSessionIndexRecord.insert(qMakePair(senderSession->sessionId(), decrypted.second), qMakePair(eventId, timestamp)); } else { - if ((properties.first != eventId) - || (properties.second != timestamp)) { - qCDebug(E2EE) << "Detected a replay attack on event" << eventId; + if ((properties.first != eventId) || (properties.second != timestamp)) { + qCWarning(E2EE) << "Detected a replay attack on event" << eventId; return QString(); } } - */ - + //TODO is this necessary? + saveMegOlmSessions(); return decrypted.first; } #endif // Quotient_E2EE_ENABLED @@ -475,6 +520,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); + qCDebug(STATE) << "New" << initialJoinState << "Room:" << id; #ifdef Quotient_E2EE_ENABLED connectSingleShot(this, &Room::encryption, this, [=](){ connection->encryptionUpdate(this); @@ -484,6 +530,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connection->encryptionUpdate(this); } }); + d->loadMegOlmSessions(); #endif qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } @@ -1504,13 +1551,29 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) encryptedEvent.sessionId(), encryptedEvent.id(), encryptedEvent.originTimestamp()); if (decrypted.isEmpty()) { + qCWarning(E2EE) << "Encrypted message is empty"; return {}; } - return makeEvent( - QJsonDocument::fromJson(decrypted.toUtf8()).object()); + QJsonObject eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + eventObject["event_id"] = encryptedEvent.id(); + eventObject["sender"] = encryptedEvent.senderId(); + eventObject["origin_server_ts"] = encryptedEvent.originTimestamp().toMSecsSinceEpoch(); + if(encryptedEvent.contentJson().contains("m.relates_to")) { + auto relates = encryptedEvent.contentJson()["m.relates_to"].toObject(); + auto content = eventObject["content"].toObject(); + content["m.relates_to"] = relates; + eventObject["content"] = content; + } + if(encryptedEvent.unsignedJson().contains("redacts")) { + auto redacts = encryptedEvent.unsignedJson()["redacts"].toString(); + auto unsign = eventObject["unsigned"].toObject(); + unsign["redacts"] = redacts; + eventObject["unsigned"] = unsign; + } + return makeEvent(eventObject); } qCDebug(E2EE) << "Algorithm of the encrypted event with id" - << encryptedEvent.id() << "is not for the current device"; + << encryptedEvent.id() << "is not decryptable by the current device"; return {}; #endif // Quotient_E2EE_ENABLED } @@ -1529,8 +1592,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, } if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), roomKeyEvent.sessionKey())) { - qCDebug(E2EE) << "added new inboundGroupSession:" - << d->groupSessions.count(); + qCWarning(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.size(); } #endif // Quotient_E2EE_ENABLED } @@ -2590,6 +2653,18 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) QElapsedTimer et; et.start(); + + //TODO should this be done before dropDuplicateEvents? + for(long unsigned int i = 0; i < events.size(); i++) { + if(auto* encrypted = eventCast(events[i])) { + qDebug() << "Encrypted Event"; + auto decrypted = q->decryptMessage(*encrypted); + if(decrypted) { + events[i] = std::move(decrypted); + } + } + } + { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -2742,6 +2817,18 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) return; Changes changes {}; + + //TODO should this be done before dropDuplicateEvents? + for(long unsigned int i = 0; i < events.size(); i++) { + if(auto* encrypted = eventCast(events[i])) { + qDebug() << "Encrypted Event"; + auto decrypted = q->decryptMessage(*encrypted); + if(decrypted) { + events[i] = std::move(decrypted); + } + } + } + // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly -- cgit v1.2.3 From 244938d2c99674ba09f3c1f92b2a4f8507ac5e58 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 17 Aug 2021 20:51:29 +0200 Subject: Various fixes --- lib/room.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 5fedd861..a1354fc5 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -372,7 +372,7 @@ public: std::map, std::unique_ptr> groupSessions; void loadMegOlmSessions() { - QFile file { connection->stateCacheDir().filePath("megolmsessions.json") }; + QFile file { connection->stateCacheDir().filePath(QStringLiteral("megolm/%1.json").arg(id)) }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No megolm sessions cache exists."; return; @@ -387,7 +387,7 @@ public: #endif ; if (json.isEmpty()) { - qCWarning(MAIN) << "Megolm sessions cache is empty"; + qCWarning(E2EE) << "Megolm sessions cache is empty"; return; } for(const auto &s : json["sessions"].toArray()) { @@ -403,7 +403,8 @@ public: } } void saveMegOlmSessions() { - QFile outFile { connection->stateCacheDir().filePath("megolmsessions.json") }; + connection->stateCacheDir().mkdir("megolm"); + QFile outFile { connection->stateCacheDir().filePath(QStringLiteral("megolm/%1.json").arg(id))}; if (!outFile.open(QFile::WriteOnly)) { qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); @@ -521,6 +522,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) return this == r; // loadedRoomState fires only once per room }); qCDebug(STATE) << "New" << initialJoinState << "Room:" << id; + #ifdef Quotient_E2EE_ENABLED connectSingleShot(this, &Room::encryption, this, [=](){ connection->encryptionUpdate(this); -- cgit v1.2.3 From 0583534d83f902235b46ef6761d6698ddb6e6aba Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 18 Aug 2021 02:00:15 +0200 Subject: Store pickling key in qtkeychain and pickle encrypted --- lib/room.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a1354fc5..b60a23f2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -394,7 +394,7 @@ public: auto pickle = s.toObject()["pickle"].toString().toLatin1(); auto senderKey = s.toObject()["sender_key"].toString(); auto sessionId = s.toObject()["session_id"].toString(); - auto sessionResult = QOlmInboundGroupSession::unpickle(pickle, Unencrypted{}); + auto sessionResult = QOlmInboundGroupSession::unpickle(pickle, connection->picklingMode()); if(std::holds_alternative(sessionResult)) { qCWarning(E2EE) << "Failed to unpickle olm session"; continue; @@ -421,7 +421,7 @@ public: { QJsonArray sessionsJson; for (const auto &session : groupSessions) { - auto pickleResult = session.second->pickle(Unencrypted{}); + auto pickleResult = session.second->pickle(connection->picklingMode()); sessionsJson += QJsonObject { {QStringLiteral("sender_key"), session.first.first}, {QStringLiteral("session_id"), session.first.second}, @@ -2659,7 +2659,6 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) //TODO should this be done before dropDuplicateEvents? for(long unsigned int i = 0; i < events.size(); i++) { if(auto* encrypted = eventCast(events[i])) { - qDebug() << "Encrypted Event"; auto decrypted = q->decryptMessage(*encrypted); if(decrypted) { events[i] = std::move(decrypted); -- cgit v1.2.3 From 77a13cfdace5cb27adb52b3a644a155aee522b12 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 28 Aug 2021 01:03:49 +0200 Subject: Implement download and decryption of encrypted files --- lib/room.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index b60a23f2..a4dfcb8f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2426,6 +2426,65 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) d->failedTransfer(eventId); } +#ifdef Quotient_E2EE_ENABLED +void Room::downloadFile(const QString& eventId, const QString& key, const QString& iv, const QString& sha256, const QUrl& localFilename) +{ + if (auto ongoingTransfer = d->fileTransfers.constFind(eventId); + ongoingTransfer != d->fileTransfers.cend() + && ongoingTransfer->status == FileTransferInfo::Started) { + qCWarning(MAIN) << "Transfer for" << eventId + << "is ongoing; download won't start"; + return; + } + + Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), + __FUNCTION__, "localFilename should point at a local file"); + const auto* event = d->getEventWithFile(eventId); + if (!event) { + qCCritical(MAIN) + << eventId << "is not in the local timeline or has no file content"; + Q_ASSERT(false); + return; + } + if (!event->contentJson().contains(QStringLiteral("file"))) { + qCWarning(MAIN) << "Event" << eventId + << "has an empty or malformed mxc URL; won't download"; + return; + } + const auto fileUrl = QUrl(event->contentJson()["file"]["url"].toString()); + auto filePath = localFilename.toLocalFile(); + if (filePath.isEmpty()) { // Setup default file path + filePath = + fileUrl.path().mid(1) % '_' % d->fileNameToDownload(event); + + if (filePath.size() > 200) // If too long, elide in the middle + filePath.replace(128, filePath.size() - 192, "---"); + + filePath = QDir::tempPath() % '/' % filePath; + qDebug(MAIN) << "File path:" << filePath; + } + auto job = connection()->downloadFile(fileUrl, key, iv, sha256, filePath); + if (isJobPending(job)) { + // If there was a previous transfer (completed or failed), overwrite it. + d->fileTransfers[eventId] = { job, job->targetFileName() }; + connect(job, &BaseJob::downloadProgress, this, + [this, eventId](qint64 received, qint64 total) { + d->fileTransfers[eventId].update(received, total); + emit fileTransferProgress(eventId, received, total); + }); + connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { + d->fileTransfers[eventId].status = FileTransferInfo::Completed; + emit fileTransferCompleted( + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); + }); + connect(job, &BaseJob::failure, this, + std::bind(&Private::failedTransfer, d, eventId, + job->errorString())); + } else + d->failedTransfer(eventId); +} +#endif + void Room::cancelFileTransfer(const QString& id) { const auto it = d->fileTransfers.find(id); -- cgit v1.2.3 From b35a736da2b09fe5cc0091f9fbd370d057503a54 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 30 Aug 2021 00:53:29 +0200 Subject: Handle encrypted file download through existing API --- lib/room.cpp | 67 ++++++++---------------------------------------------------- 1 file changed, 9 insertions(+), 58 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a4dfcb8f..d7ebe021 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2405,65 +2405,17 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) filePath = QDir::tempPath() % '/' % filePath; qDebug(MAIN) << "File path:" << filePath; } - auto job = connection()->downloadFile(fileUrl, filePath); - if (isJobPending(job)) { - // If there was a previous transfer (completed or failed), overwrite it. - d->fileTransfers[eventId] = { job, job->targetFileName() }; - connect(job, &BaseJob::downloadProgress, this, - [this, eventId](qint64 received, qint64 total) { - d->fileTransfers[eventId].update(received, total); - emit fileTransferProgress(eventId, received, total); - }); - connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { - d->fileTransfers[eventId].status = FileTransferInfo::Completed; - emit fileTransferCompleted( - eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); - }); - connect(job, &BaseJob::failure, this, - std::bind(&Private::failedTransfer, d, eventId, - job->errorString())); - } else - d->failedTransfer(eventId); -} - + DownloadFileJob *job = nullptr; +#ifdef Quotient_E2EE_ENABLED + if(fileInfo->file.has_value()) { + auto file = *fileInfo->file; + job = connection()->downloadFile(fileUrl, file.key.k, file.iv, file.hashes["sha256"], filePath); + } else { +#endif + job = connection()->downloadFile(fileUrl, filePath); #ifdef Quotient_E2EE_ENABLED -void Room::downloadFile(const QString& eventId, const QString& key, const QString& iv, const QString& sha256, const QUrl& localFilename) -{ - if (auto ongoingTransfer = d->fileTransfers.constFind(eventId); - ongoingTransfer != d->fileTransfers.cend() - && ongoingTransfer->status == FileTransferInfo::Started) { - qCWarning(MAIN) << "Transfer for" << eventId - << "is ongoing; download won't start"; - return; - } - - Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), - __FUNCTION__, "localFilename should point at a local file"); - const auto* event = d->getEventWithFile(eventId); - if (!event) { - qCCritical(MAIN) - << eventId << "is not in the local timeline or has no file content"; - Q_ASSERT(false); - return; - } - if (!event->contentJson().contains(QStringLiteral("file"))) { - qCWarning(MAIN) << "Event" << eventId - << "has an empty or malformed mxc URL; won't download"; - return; - } - const auto fileUrl = QUrl(event->contentJson()["file"]["url"].toString()); - auto filePath = localFilename.toLocalFile(); - if (filePath.isEmpty()) { // Setup default file path - filePath = - fileUrl.path().mid(1) % '_' % d->fileNameToDownload(event); - - if (filePath.size() > 200) // If too long, elide in the middle - filePath.replace(128, filePath.size() - 192, "---"); - - filePath = QDir::tempPath() % '/' % filePath; - qDebug(MAIN) << "File path:" << filePath; } - auto job = connection()->downloadFile(fileUrl, key, iv, sha256, filePath); +#endif if (isJobPending(job)) { // If there was a previous transfer (completed or failed), overwrite it. d->fileTransfers[eventId] = { job, job->targetFileName() }; @@ -2483,7 +2435,6 @@ void Room::downloadFile(const QString& eventId, const QString& key, const QStrin } else d->failedTransfer(eventId); } -#endif void Room::cancelFileTransfer(const QString& id) { -- cgit v1.2.3 From 8636c7028b45ee8de3125bcf4df40ad60ed949a0 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 00:09:59 +0200 Subject: Add mxc protocol to the networkaccessmanager --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index d7ebe021..688ba5d4 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2409,7 +2409,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) #ifdef Quotient_E2EE_ENABLED if(fileInfo->file.has_value()) { auto file = *fileInfo->file; - job = connection()->downloadFile(fileUrl, file.key.k, file.iv, file.hashes["sha256"], filePath); + job = connection()->downloadFile(fileUrl, file, filePath); } else { #endif job = connection()->downloadFile(fileUrl, filePath); -- cgit v1.2.3 From 82cffec29937e4449a75040485d5188f429b7b1e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 10 Nov 2021 16:29:11 +0100 Subject: Try decrypting existing messages when a new key is added --- lib/room.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 688ba5d4..6c5a9d33 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1596,6 +1596,14 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, roomKeyEvent.sessionKey())) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); + for (unsigned long int i = 0; i < d->timeline.size(); i++) { + if (auto encryptedEvent = d->timeline[i].viewAs()) { + auto decrypted = decryptMessage(*encryptedEvent); + if(decrypted) { + d->timeline[i].replaceEvent(std::move(decrypted)); + } + } + } } #endif // Quotient_E2EE_ENABLED } -- cgit v1.2.3 From 34db4fd1294e41765a5db58ee1a0c59712af62c6 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 15 Nov 2021 21:26:47 +0100 Subject: Various improvements and fixes --- lib/room.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6c5a9d33..94f0c9eb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1600,7 +1600,9 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, if (auto encryptedEvent = d->timeline[i].viewAs()) { auto decrypted = decryptMessage(*encryptedEvent); if(decrypted) { - d->timeline[i].replaceEvent(std::move(decrypted)); + qWarning() << "decrypted" << decrypted->fullJson(); + auto oldEvent = d->timeline[i].replaceEvent(std::move(decrypted)); + emit replacedEvent(d->timeline[i].event(), rawPtr(oldEvent)); } } } -- cgit v1.2.3 From 06facdb1179e2e6789d7263541294fb427f649e5 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 15 Nov 2021 21:57:59 +0100 Subject: Move non-cache data to a non-cache location --- lib/room.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 94f0c9eb..963b9f88 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -372,7 +372,7 @@ public: std::map, std::unique_ptr> groupSessions; void loadMegOlmSessions() { - QFile file { connection->stateCacheDir().filePath(QStringLiteral("megolm/%1.json").arg(id)) }; + QFile file { connection->e2eeDataDir() + QStringLiteral("/%1.json").arg(id) }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No megolm sessions cache exists."; return; @@ -403,8 +403,7 @@ public: } } void saveMegOlmSessions() { - connection->stateCacheDir().mkdir("megolm"); - QFile outFile { connection->stateCacheDir().filePath(QStringLiteral("megolm/%1.json").arg(id))}; + QFile outFile { connection->e2eeDataDir() + QStringLiteral("/%1.json").arg(id)}; if (!outFile.open(QFile::WriteOnly)) { qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); -- cgit v1.2.3 From 15e75b20d5bb9339a8b769b717db00fb5c16b050 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 16 Nov 2021 23:23:26 +0100 Subject: Add function to decrypt notifications --- lib/room.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 963b9f88..d755f8eb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1599,7 +1599,6 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, if (auto encryptedEvent = d->timeline[i].viewAs()) { auto decrypted = decryptMessage(*encryptedEvent); if(decrypted) { - qWarning() << "decrypted" << decrypted->fullJson(); auto oldEvent = d->timeline[i].replaceEvent(std::move(decrypted)); emit replacedEvent(d->timeline[i].event(), rawPtr(oldEvent)); } -- cgit v1.2.3 From 877591582f07b5c5c104370e80c858b951c0757f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 20 Nov 2021 16:58:40 +0100 Subject: Use UnorderedMap instead of std::map --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index d755f8eb..65ce82ac 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -369,7 +369,7 @@ public: // A map from senderKey to a map of sessionId to InboundGroupSession // Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. - std::map, std::unique_ptr> groupSessions; + UnorderedMap, std::unique_ptr> groupSessions; void loadMegOlmSessions() { QFile file { connection->e2eeDataDir() + QStringLiteral("/%1.json").arg(id) }; -- cgit v1.2.3 From e99802772ebab9802e2f35d83ce1de9f83691d90 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 27 Nov 2021 00:11:36 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/room.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 65ce82ac..fca1912f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -456,7 +456,7 @@ public: return false; } qCWarning(E2EE) << "Adding inbound session"; - groupSessions[qMakePair(senderKey, sessionId)] = std::move(megolmSession); + groupSessions[{senderKey, sessionId}] = std::move(megolmSession); saveMegOlmSessions(); return true; } @@ -469,13 +469,14 @@ public: { QPair senderSessionPairKey = qMakePair(senderKey, sessionId); - if (groupSessions.find(senderSessionPairKey) == groupSessions.end()) { + auto groupSessionIt = groupSessions.find(senderSessionPairKey); + if (groupSessionIt == groupSessions.end()) { qCWarning(E2EE) << "Unable to decrypt event" << eventId << "The sender's device has not sent us the keys for " "this message"; return QString(); } - auto& senderSession = groupSessions[senderSessionPairKey]; + auto& senderSession = *groupSessionIt; auto decryptResult = senderSession->decrypt(cipher); if(std::holds_alternative(decryptResult)) { qCWarning(E2EE) << "Unable to decrypt event" << eventId @@ -520,8 +521,6 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); - qCDebug(STATE) << "New" << initialJoinState << "Room:" << id; - #ifdef Quotient_E2EE_ENABLED connectSingleShot(this, &Room::encryption, this, [=](){ connection->encryptionUpdate(this); @@ -1555,7 +1554,7 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) qCWarning(E2EE) << "Encrypted message is empty"; return {}; } - QJsonObject eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); eventObject["event_id"] = encryptedEvent.id(); eventObject["sender"] = encryptedEvent.senderId(); eventObject["origin_server_ts"] = encryptedEvent.originTimestamp().toMSecsSinceEpoch(); -- cgit v1.2.3 From 8020505eb582479cf62d0157fe866f63a888f1a9 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 27 Nov 2021 00:52:24 +0100 Subject: Apply more suggestions --- lib/room.cpp | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index fca1912f..e143747b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -430,12 +430,7 @@ public: rootObj.insert(QStringLiteral("sessions"), sessionsJson); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { rootObj }; - const auto data = json.toJson(QJsonDocument::Compact); -#endif outFile.write(data.data(), data.size()); qCDebug(E2EE) << "Megolm sessions saved to" << outFile.fileName(); @@ -476,26 +471,26 @@ public: "this message"; return QString(); } - auto& senderSession = *groupSessionIt; + auto& senderSession = groupSessionIt->second; auto decryptResult = senderSession->decrypt(cipher); if(std::holds_alternative(decryptResult)) { qCWarning(E2EE) << "Unable to decrypt event" << eventId << "with matching megolm session:" << std::get(decryptResult); return QString(); } - std::pair decrypted = std::get>(decryptResult); - QPair properties = groupSessionIndexRecord.value(qMakePair(senderSession->sessionId(), decrypted.second)); - if (properties.first.isEmpty()) { - groupSessionIndexRecord.insert(qMakePair(senderSession->sessionId(), decrypted.second), qMakePair(eventId, timestamp)); + const auto& [content, index] = std::get>(decryptResult); + const auto& [recordEventId, ts] = groupSessionIndexRecord.value({senderSession->sessionId(), index}); + if (eventId.isEmpty()) { + groupSessionIndexRecord.insert({senderSession->sessionId(), index}, {recordEventId, timestamp}); } else { - if ((properties.first != eventId) || (properties.second != timestamp)) { + if ((eventId != recordEventId) || (ts != timestamp)) { qCWarning(E2EE) << "Detected a replay attack on event" << eventId; return QString(); } } //TODO is this necessary? saveMegOlmSessions(); - return decrypted.first; + return content; } #endif // Quotient_E2EE_ENABLED @@ -2673,7 +2668,6 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) QElapsedTimer et; et.start(); - //TODO should this be done before dropDuplicateEvents? for(long unsigned int i = 0; i < events.size(); i++) { if(auto* encrypted = eventCast(events[i])) { auto decrypted = q->decryptMessage(*encrypted); @@ -2836,7 +2830,6 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) Changes changes {}; - //TODO should this be done before dropDuplicateEvents? for(long unsigned int i = 0; i < events.size(); i++) { if(auto* encrypted = eventCast(events[i])) { qDebug() << "Encrypted Event"; -- cgit v1.2.3 From 3128df9daa196b2cf3cdb8e029e22d79c397ff66 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 27 Nov 2021 01:34:44 +0100 Subject: Apply even more suggestions --- lib/room.cpp | 53 +++++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 36 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e143747b..07ffd0cd 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -366,9 +366,7 @@ public: // A map from to QHash, QPair> groupSessionIndexRecord; // TODO: cache - // A map from senderKey to a map of sessionId to InboundGroupSession - // Not using QMultiHash, because we want to quickly return - // a number of relations for a given event without enumerating them. + // A map from (senderKey, sessionId) to InboundGroupSession UnorderedMap, std::unique_ptr> groupSessions; void loadMegOlmSessions() { @@ -399,7 +397,7 @@ public: qCWarning(E2EE) << "Failed to unpickle olm session"; continue; } - groupSessions[qMakePair(senderKey, sessionId)] = std::move(std::get>(sessionResult)); + groupSessions[{senderKey, sessionId}] = std::move(std::get>(sessionResult)); } } void saveMegOlmSessions() { @@ -438,7 +436,7 @@ public: bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) { - if (groupSessions.find(qMakePair(senderKey, sessionId)) != groupSessions.end()) { + if (groupSessions.find({senderKey, sessionId}) != groupSessions.end()) { qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "with senderKey" << senderKey << "already exists"; return false; @@ -462,8 +460,7 @@ public: const QString& eventId, QDateTime timestamp) { - QPair senderSessionPairKey = - qMakePair(senderKey, sessionId); + const auto senderSessionPairKey = qMakePair(senderKey, sessionId); auto groupSessionIt = groupSessions.find(senderSessionPairKey); if (groupSessionIt == groupSessions.end()) { qCWarning(E2EE) << "Unable to decrypt event" << eventId @@ -1540,36 +1537,20 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; #else // Quotient_E2EE_ENABLED - if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) { - QString decrypted = d->groupSessionDecryptMessage( - encryptedEvent.ciphertext(), encryptedEvent.senderKey(), - encryptedEvent.sessionId(), encryptedEvent.id(), - encryptedEvent.originTimestamp()); - if (decrypted.isEmpty()) { - qCWarning(E2EE) << "Encrypted message is empty"; - return {}; - } - auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); - eventObject["event_id"] = encryptedEvent.id(); - eventObject["sender"] = encryptedEvent.senderId(); - eventObject["origin_server_ts"] = encryptedEvent.originTimestamp().toMSecsSinceEpoch(); - if(encryptedEvent.contentJson().contains("m.relates_to")) { - auto relates = encryptedEvent.contentJson()["m.relates_to"].toObject(); - auto content = eventObject["content"].toObject(); - content["m.relates_to"] = relates; - eventObject["content"] = content; - } - if(encryptedEvent.unsignedJson().contains("redacts")) { - auto redacts = encryptedEvent.unsignedJson()["redacts"].toString(); - auto unsign = eventObject["unsigned"].toObject(); - unsign["redacts"] = redacts; - eventObject["unsigned"] = unsign; - } - return makeEvent(eventObject); + if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) { + qWarning(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not decryptable by the current device"; + return {}; } - qCDebug(E2EE) << "Algorithm of the encrypted event with id" - << encryptedEvent.id() << "is not decryptable by the current device"; - return {}; + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.senderKey(), + encryptedEvent.sessionId(), encryptedEvent.id(), + encryptedEvent.originTimestamp()); + if (decrypted.isEmpty()) { + qCWarning(E2EE) << "Encrypted message is empty"; + return {}; + } + return encryptedEvent.createDecrypted(decrypted); #endif // Quotient_E2EE_ENABLED } -- cgit v1.2.3 From dcc4556a761f96ae6c71115bf6297feca32581bf Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 27 Nov 2021 01:58:02 +0100 Subject: More improvements --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 07ffd0cd..e4fe2fb8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -514,10 +514,10 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) return this == r; // loadedRoomState fires only once per room }); #ifdef Quotient_E2EE_ENABLED - connectSingleShot(this, &Room::encryption, this, [=](){ + connectSingleShot(this, &Room::encryption, this, [this, connection](){ connection->encryptionUpdate(this); }); - connect(this, &Room::userAdded, this, [=](){ + connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); } -- cgit v1.2.3 From ae0ad49f36e8ba5983839581302ed16ddbd75d5f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 2 Dec 2021 14:04:49 +0100 Subject: visit(Event, ...) -> switchOnType() It has not much to do with the Visitor design pattern; also, std::visit() has different conventions on the order of parameters. --- lib/room.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4d60570d..b3438e08 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2779,7 +2779,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change // clang-format off - const bool proceed = visit(e + const bool proceed = switchOnType(e , [this, curStateEvent](const RoomMemberEvent& rme) { // clang-format on auto* oldRme = static_cast(curStateEvent); @@ -2877,7 +2877,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // Update internal structures as per the change and work out the return value // clang-format off - const auto result = visit(e + const auto result = switchOnType(e , [] (const RoomNameEvent&) { return Change::Name; } @@ -2885,7 +2885,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // clang-format on setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); const auto* oldCae = - static_cast(oldStateEvent); + static_cast(oldStateEvent); QStringList previousAltAliases {}; if (oldCae) { previousAltAliases = oldCae->altAliases(); @@ -2897,7 +2897,8 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!cae.alias().isEmpty()) newAliases.push_front(cae.alias()); - connection()->updateRoomAliases(id(), previousAltAliases, newAliases); + connection()->updateRoomAliases(id(), previousAltAliases, + newAliases); return Change::Aliases; // clang-format off } -- cgit v1.2.3 From 47bd4dfb2bc720d2b5919b93985f87d918af572a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 00:25:05 +0100 Subject: Port E2EE to database instead of JSON files --- lib/room.cpp | 82 +++++++----------------------------------------------------- 1 file changed, 9 insertions(+), 73 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e4fe2fb8..8181f16a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -70,6 +70,8 @@ #include "crypto/qolminboundsession.h" #endif // Quotient_E2EE_ENABLED +#include "database.h" + using namespace Quotient; using namespace std::placeholders; using std::move; @@ -363,75 +365,11 @@ public: bool isLocalUser(const User* u) const { return u == q->localUser(); } #ifdef Quotient_E2EE_ENABLED - // A map from to - QHash, QPair> - groupSessionIndexRecord; // TODO: cache // A map from (senderKey, sessionId) to InboundGroupSession - UnorderedMap, std::unique_ptr> groupSessions; + UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; void loadMegOlmSessions() { - QFile file { connection->e2eeDataDir() + QStringLiteral("/%1.json").arg(id) }; - if(!file.exists() || !file.open(QIODevice::ReadOnly)) { - qCDebug(E2EE) << "No megolm sessions cache exists."; - return; - } - auto data = file.readAll(); - const auto json = data.startsWith('{') - ? QJsonDocument::fromJson(data).object() -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - : QCborValue::fromCbor(data).toJsonValue().toObject() -#else - : QJsonDocument::fromBinaryData(data).object() -#endif - ; - if (json.isEmpty()) { - qCWarning(E2EE) << "Megolm sessions cache is empty"; - return; - } - for(const auto &s : json["sessions"].toArray()) { - auto pickle = s.toObject()["pickle"].toString().toLatin1(); - auto senderKey = s.toObject()["sender_key"].toString(); - auto sessionId = s.toObject()["session_id"].toString(); - auto sessionResult = QOlmInboundGroupSession::unpickle(pickle, connection->picklingMode()); - if(std::holds_alternative(sessionResult)) { - qCWarning(E2EE) << "Failed to unpickle olm session"; - continue; - } - groupSessions[{senderKey, sessionId}] = std::move(std::get>(sessionResult)); - } - } - void saveMegOlmSessions() { - QFile outFile { connection->e2eeDataDir() + QStringLiteral("/%1.json").arg(id)}; - if (!outFile.open(QFile::WriteOnly)) { - qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" - << outFile.errorString(); - qCWarning(E2EE) << "Failed to write megolm sessions"; - return; - } - - QJsonObject rootObj { - { QStringLiteral("cache_version"), - QJsonObject { - { QStringLiteral("major"), 1 }, - { QStringLiteral("minor"), 0 } } } - }; - { - QJsonArray sessionsJson; - for (const auto &session : groupSessions) { - auto pickleResult = session.second->pickle(connection->picklingMode()); - sessionsJson += QJsonObject { - {QStringLiteral("sender_key"), session.first.first}, - {QStringLiteral("session_id"), session.first.second}, - {QStringLiteral("pickle"), QString(pickleResult)} - }; - } - rootObj.insert(QStringLiteral("sessions"), sessionsJson); - } - - const auto data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); - - outFile.write(data.data(), data.size()); - qCDebug(E2EE) << "Megolm sessions saved to" << outFile.fileName(); + groupSessions = Database::instance().loadMegolmSessions(q->localUser()->id(), q->id(), q->connection()->picklingMode()); } bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) @@ -449,8 +387,8 @@ public: return false; } qCWarning(E2EE) << "Adding inbound session"; + Database::instance().saveMegolmSession(q->localUser()->id(), q->id(), senderKey, sessionId, megolmSession->pickle(q->connection()->picklingMode())); groupSessions[{senderKey, sessionId}] = std::move(megolmSession); - saveMegOlmSessions(); return true; } @@ -476,17 +414,15 @@ public: return QString(); } const auto& [content, index] = std::get>(decryptResult); - const auto& [recordEventId, ts] = groupSessionIndexRecord.value({senderSession->sessionId(), index}); - if (eventId.isEmpty()) { - groupSessionIndexRecord.insert({senderSession->sessionId(), index}, {recordEventId, timestamp}); + const auto& [recordEventId, ts] = Database::instance().groupSessionIndexRecord(q->localUser()->id(), q->id(), senderSession->sessionId(), index); + if (recordEventId.isEmpty()) { + Database::instance().addGroupSessionIndexRecord(q->localUser()->id(), q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch()); } else { - if ((eventId != recordEventId) || (ts != timestamp)) { + if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) { qCWarning(E2EE) << "Detected a replay attack on event" << eventId; return QString(); } } - //TODO is this necessary? - saveMegOlmSessions(); return content; } #endif // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 2c6fa33ca52842e9dfba0dd3893a9d5526e10e60 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 19:08:29 +0100 Subject: Rename "crypto" -> "e2ee" --- lib/room.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 8181f16a..755f677a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,7 +12,6 @@ #include "avatar.h" #include "connection.h" #include "converters.h" -#include "crypto/e2ee.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -65,9 +64,10 @@ #include #ifdef Quotient_E2EE_ENABLED -#include "crypto/qolmaccount.h" -#include "crypto/qolmerrors.h" -#include "crypto/qolminboundsession.h" +#include "e2ee/e2ee.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolminboundsession.h" #endif // Quotient_E2EE_ENABLED #include "database.h" -- cgit v1.2.3 From 5cf182fd4fed95e1a16936f400e8ff6fcf991d7c Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 19:18:35 +0100 Subject: Fixes --- lib/room.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 755f677a..15cbac28 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -401,9 +401,9 @@ public: const auto senderSessionPairKey = qMakePair(senderKey, sessionId); auto groupSessionIt = groupSessions.find(senderSessionPairKey); if (groupSessionIt == groupSessions.end()) { - qCWarning(E2EE) << "Unable to decrypt event" << eventId - << "The sender's device has not sent us the keys for " - "this message"; + // qCWarning(E2EE) << "Unable to decrypt event" << eventId + // << "The sender's device has not sent us the keys for " + // "this message"; return QString(); } auto& senderSession = groupSessionIt->second; @@ -1483,7 +1483,7 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) encryptedEvent.sessionId(), encryptedEvent.id(), encryptedEvent.originTimestamp()); if (decrypted.isEmpty()) { - qCWarning(E2EE) << "Encrypted message is empty"; + // qCWarning(E2EE) << "Encrypted message is empty"; return {}; } return encryptedEvent.createDecrypted(decrypted); @@ -2749,7 +2749,6 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) for(long unsigned int i = 0; i < events.size(); i++) { if(auto* encrypted = eventCast(events[i])) { - qDebug() << "Encrypted Event"; auto decrypted = q->decryptMessage(*encrypted); if(decrypted) { events[i] = std::move(decrypted); -- cgit v1.2.3 From 9fb07da8451f024085061e2985e9be384e7beb5c Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 23:12:04 +0100 Subject: Maintain list of undecrypted events to speed up decryption of old messages --- lib/room.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 15cbac28..8e348089 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -138,6 +138,8 @@ public: QString prevBatch; QPointer eventsHistoryJob; QPointer allMembersJob; + // Map from megolm sessionId to set of eventIds + UnorderedMap> undecryptedEvents; struct FileTransferPrivateInfo { FileTransferPrivateInfo() = default; @@ -1506,12 +1508,17 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, roomKeyEvent.sessionKey())) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); - for (unsigned long int i = 0; i < d->timeline.size(); i++) { - if (auto encryptedEvent = d->timeline[i].viewAs()) { + for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) { + if (!d->eventsIndex.contains(eventId)) { + continue; + } + auto event = d->timeline.rend() - (d->eventsIndex.value(eventId) - minTimelineIndex() + 1); + if (auto encryptedEvent = event->viewAs()) { auto decrypted = decryptMessage(*encryptedEvent); if(decrypted) { - auto oldEvent = d->timeline[i].replaceEvent(std::move(decrypted)); - emit replacedEvent(d->timeline[i].event(), rawPtr(oldEvent)); + auto oldEvent = event->replaceEvent(std::move(decrypted)); + emit replacedEvent(event->event(), rawPtr(oldEvent)); + d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; } } } @@ -2590,6 +2597,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) auto decrypted = q->decryptMessage(*encrypted); if(decrypted) { events[i] = std::move(decrypted); + } else { + undecryptedEvents[encrypted->sessionId()] += encrypted->id(); } } } @@ -2752,6 +2761,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) auto decrypted = q->decryptMessage(*encrypted); if(decrypted) { events[i] = std::move(decrypted); + } else { + undecryptedEvents[encrypted->sessionId()] += encrypted->id(); } } } -- cgit v1.2.3 From a0ce17dfe793c924205b449c026f2f776b032ff3 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 8 Dec 2021 23:07:14 +0100 Subject: Store encryptedevent in decrypted roomevents --- lib/room.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 8e348089..b3a092f3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1517,6 +1517,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, auto decrypted = decryptMessage(*encryptedEvent); if(decrypted) { auto oldEvent = event->replaceEvent(std::move(decrypted)); + decrypted->setOriginalEvent(std::move(oldEvent)); emit replacedEvent(event->event(), rawPtr(oldEvent)); d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; } @@ -2596,7 +2597,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if(auto* encrypted = eventCast(events[i])) { auto decrypted = q->decryptMessage(*encrypted); if(decrypted) { - events[i] = std::move(decrypted); + auto oldEvent = std::exchange(events[i], std::move(decrypted)); + events[i]->setOriginalEvent(std::move(oldEvent)); } else { undecryptedEvents[encrypted->sessionId()] += encrypted->id(); } @@ -2760,7 +2762,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) if(auto* encrypted = eventCast(events[i])) { auto decrypted = q->decryptMessage(*encrypted); if(decrypted) { - events[i] = std::move(decrypted); + auto oldEvent = std::exchange(events[i], std::move(decrypted)); + events[i]->setOriginalEvent(std::move(oldEvent)); } else { undecryptedEvents[encrypted->sessionId()] += encrypted->id(); } -- cgit v1.2.3 From 58798ce15f0f235d64f9c34b3f8c013678ebf25f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 9 Dec 2021 23:26:24 +0100 Subject: Ifdef all the things --- lib/room.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index b3a092f3..7d608520 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -68,9 +68,9 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" +#include "database.h" #endif // Quotient_E2EE_ENABLED -#include "database.h" using namespace Quotient; using namespace std::placeholders; @@ -2593,6 +2593,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) QElapsedTimer et; et.start(); +#ifdef Quotient_E2EE_ENABLED for(long unsigned int i = 0; i < events.size(); i++) { if(auto* encrypted = eventCast(events[i])) { auto decrypted = q->decryptMessage(*encrypted); @@ -2604,6 +2605,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } } } +#endif { // Pre-process redactions and edits so that events that get @@ -2758,6 +2760,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) Changes changes {}; +#ifdef Quotient_E2EE_ENABLED for(long unsigned int i = 0; i < events.size(); i++) { if(auto* encrypted = eventCast(events[i])) { auto decrypted = q->decryptMessage(*encrypted); @@ -2769,6 +2772,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) } } } +#endif // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; -- cgit v1.2.3 From b4cc38fc7c2c63d8122106a2451aec2c60176a4b Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 10 Dec 2021 16:10:10 +0100 Subject: Use individual databases for each connection --- lib/room.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7d608520..458f870d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -371,7 +371,7 @@ public: UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; void loadMegOlmSessions() { - groupSessions = Database::instance().loadMegolmSessions(q->localUser()->id(), q->id(), q->connection()->picklingMode()); + groupSessions = q->connection()->database()->loadMegolmSessions(q->id(), q->connection()->picklingMode()); } bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) @@ -389,7 +389,7 @@ public: return false; } qCWarning(E2EE) << "Adding inbound session"; - Database::instance().saveMegolmSession(q->localUser()->id(), q->id(), senderKey, sessionId, megolmSession->pickle(q->connection()->picklingMode())); + q->connection()->database()->saveMegolmSession(q->id(), senderKey, sessionId, megolmSession->pickle(q->connection()->picklingMode())); groupSessions[{senderKey, sessionId}] = std::move(megolmSession); return true; } @@ -416,9 +416,9 @@ public: return QString(); } const auto& [content, index] = std::get>(decryptResult); - const auto& [recordEventId, ts] = Database::instance().groupSessionIndexRecord(q->localUser()->id(), q->id(), senderSession->sessionId(), index); + const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index); if (recordEventId.isEmpty()) { - Database::instance().addGroupSessionIndexRecord(q->localUser()->id(), q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch()); + q->connection()->database()->addGroupSessionIndexRecord(q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch()); } else { if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) { qCWarning(E2EE) << "Detected a replay attack on event" << eventId; -- cgit v1.2.3 From 4a17403f9adad9b4390f7e0010c0f7e23a718f7b Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Fri, 10 Dec 2021 16:25:18 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/room.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 458f870d..0a4fcc68 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1509,16 +1509,18 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) { - if (!d->eventsIndex.contains(eventId)) { + const auto pIdx = d->eventsIndex.constFind(eventId); + if (pIdx == d->eventsIndex.cend()) continue; - } - auto event = d->timeline.rend() - (d->eventsIndex.value(eventId) - minTimelineIndex() + 1); - if (auto encryptedEvent = event->viewAs()) { + auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())]; + if (auto encryptedEvent = ti.viewAs()) { auto decrypted = decryptMessage(*encryptedEvent); if(decrypted) { - auto oldEvent = event->replaceEvent(std::move(decrypted)); - decrypted->setOriginalEvent(std::move(oldEvent)); - emit replacedEvent(event->event(), rawPtr(oldEvent)); + // The reference will survive the pointer being moved + auto& decryptedEvent = *decrypted; + auto oldEvent = ti.replaceEvent(std::move(decrypted)); + decryptedEvent.setOriginalEvent(std::move(oldEvent)); + emit replacedEvent(ti.event(), decrypted->originalEvent()); d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; } } -- cgit v1.2.3 From 6b29d759a47012eef74948e72c0d0395eb6bf282 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 10 Dec 2021 17:01:05 +0100 Subject: Remove data from database when leaving room --- lib/room.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 0a4fcc68..a46892f3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -461,6 +461,10 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) } }); d->loadMegOlmSessions(); + + connect(this, &Room::beforeDestruction, this, [=](){ + connection->database()->clearRoomData(id); + }); #endif qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } -- cgit v1.2.3 From 783d83012551df128d044382f6dd2047c5269700 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Jan 2022 12:48:28 +0100 Subject: OtherChange is Change::Other now `Room::Change` has been changed to be an enum class recently; and it's values are no more suffixed with `Change`. --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6854879c..edf5dcd9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2925,7 +2925,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) } , [this] (const RoomPinnedEvent&) { emit pinnedEventsChanged(); - return OtherChange; + return Change::Other; } , [] (const RoomTopicEvent&) { return Change::Topic; -- cgit v1.2.3 From 703e5e8c1b48bea7bd82906967cb7651f7e96751 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 15:53:21 +0100 Subject: Cleanup Room::pinnedEvents() Use 'auto'; range-for instead of an iterator loop. --- lib/room.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index edf5dcd9..55efb5b9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -562,16 +562,14 @@ QStringList Room::pinnedEventIds() const { return d->getCurrentState()->pinnedEvents(); } -QVector< const Quotient::RoomEvent* > Quotient::Room::pinnedEvents() const +QVector Quotient::Room::pinnedEvents() const { - QStringList events = d->getCurrentState()->pinnedEvents(); + const auto& pinnedIds = d->getCurrentState()->pinnedEvents(); QVector pinnedEvents; - QStringList::iterator i; - for (i = events.begin(); i != events.end(); ++i) { - auto timelineItem = findInTimeline(*i); - if (timelineItem != historyEdge()) - pinnedEvents.append(timelineItem->event()); - } + for (auto&& evtId: pinnedIds) + if (const auto& it = findInTimeline(evtId); it != historyEdge()) + pinnedEvents.append(it->event()); + return pinnedEvents; } -- cgit v1.2.3 From fdff209744ac4c422f63fe2549aa0132df7e6292 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 21 Jan 2022 02:04:10 +0100 Subject: Redo EventRelation; deprecate RelatesTo RelatesTo and EventRelation have been two means to the same end in two different contexts. (Modernised) EventRelation is the one used now both for ReactionEvent and EventContent::TextContent. The modernisation mostly boils down to using inline variables instead of functions to return relation types and switching to QLatin1String from const char* (because we know exactly that those constants are Latin-1 and QLatin1String is more efficient than const char* to compare/convert to QString). --- lib/room.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 55efb5b9..ba63f50d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -996,14 +996,14 @@ Room::findPendingEvent(const QString& txnId) const }); } -const Room::RelatedEvents Room::relatedEvents(const QString& evtId, - const char* relType) const +const Room::RelatedEvents Room::relatedEvents( + const QString& evtId, EventRelation::reltypeid_t relType) const { return d->relations.value({ evtId, relType }); } -const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, - const char* relType) const +const Room::RelatedEvents Room::relatedEvents( + const RoomEvent& evt, EventRelation::reltypeid_t relType) const { return relatedEvents(evt.id(), relType); } @@ -2506,8 +2506,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } if (const auto* reaction = eventCast(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; - const auto lookupKey = - qMakePair(targetEvtId, EventRelation::Annotation()); + const QPair lookupKey { targetEvtId, EventRelation::AnnotationType }; if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); emit q->updatedEvent(targetEvtId); -- cgit v1.2.3 From 8183a33c130f1284404edc61767ff6d29402d200 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 23 Jan 2022 17:11:45 +0100 Subject: RoomStateView This class is called to provide an arbitrary snapshot of a room state; as the first step, Room::currentState() returns an instance of this class that stores, well, the current state. Implelementation-wise it's the same hash map of two-part state event keys to const event pointers; however, RoomStateView provides additional operations: - get(), that deprecates Room::getCurrentState(), returns a pointer to a particular event if the current state has it. Unlike the original method, the pointer returned from this one can be nullptr; this is done to get rid of stubbed state events that have to be created everytime a "state miss" occurred (i.e., when getCurrentState() does not find an existing event in the current state). - eventsOfType() - this is a new place for Room::stateEventsOfType() introduced recently. - query() - this is a way to specify a piece of the state content that you need to retrieve by passing a member function or a function object that retrieves it. That is especially convenient with member functions of the event class; just pass the pointer to this member function, and query() will parse the event type it has to retrieve out of it and call that member function on the event object. Returns an Omittable<>; if the respective piece of state doesn't exist, you'll get `Quotient::none` (the same as `std::nullopt`). - queryOr() - the same but with the fallback value; instead of an Omittable<>, the fallback value will be returned if the needed event is not found. --- lib/room.cpp | 150 ++++++++++++++++++++++++++++------------------------------- 1 file changed, 72 insertions(+), 78 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ba63f50d..1450eb3b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -16,6 +16,7 @@ #include "syncdata.h" #include "user.h" #include "eventstats.h" +#include "roomstateview.h" // NB: since Qt 6, moc_room.cpp needs User fully defined #include "moc_room.cpp" @@ -103,7 +104,7 @@ public: static decltype(baseState) stubbedState; /// The state of the room at syncEdge() /// \sa syncEdge - QHash currentState; + RoomStateView currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases QSet aliasServers; @@ -227,34 +228,6 @@ public: return evt; } - QVector stateEventsOfType(const QString& evtType) const - { - auto vals = QVector(); - for (auto it = currentState.cbegin(); it != currentState.cend(); ++it) - if (it.key().first == evtType) - vals.append(it.value()); - - return vals; - } - - template - const EventT* getCurrentState(const QString& stateKey = {}) const - { - const auto* evt = getCurrentState({ EventT::matrixTypeId(), stateKey }); - Q_ASSERT(evt->type() == EventT::typeId() - && evt->matrixType() == EventT::matrixTypeId()); - return static_cast(evt); - } - -// template -// const auto& getCurrentStateContent(const QString& stateKey = {}) const -// { -// if (const auto* evt = -// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr)) -// return evt->content(); -// return EventT::content_type() -// } - template Changes updateStateFrom(EventArrayT&& events) { @@ -480,8 +453,8 @@ const QString& Room::id() const { return d->id; } QString Room::version() const { - const auto v = d->getCurrentState()->version(); - return v.isEmpty() ? QStringLiteral("1") : v; + const auto v = currentState().query(&RoomCreateEvent::version); + return v && !v->isEmpty() ? *v : QStringLiteral("1"); } bool Room::isUnstable() const @@ -492,7 +465,10 @@ bool Room::isUnstable() const QString Room::predecessorId() const { - return d->getCurrentState()->predecessor().roomId; + if (const auto* evt = currentState().get()) + return evt->predecessor().roomId; + + return {}; } Room* Room::predecessor(JoinStates statesFilter) const @@ -507,7 +483,8 @@ Room* Room::predecessor(JoinStates statesFilter) const QString Room::successorId() const { - return d->getCurrentState()->successorRoomId(); + return currentState().queryOr(&RoomTombstoneEvent::successorRoomId, + QString()); } Room* Room::successor(JoinStates statesFilter) const @@ -534,39 +511,41 @@ bool Room::allHistoryLoaded() const QString Room::name() const { - return d->getCurrentState()->name(); + return currentState().queryOr(&RoomNameEvent::name, QString()); } QStringList Room::aliases() const { - const auto* evt = d->getCurrentState(); - auto result = evt->altAliases(); - if (!evt->alias().isEmpty()) - result << evt->alias(); - return result; + if (const auto* evt = currentState().get()) { + auto result = evt->altAliases(); + if (!evt->alias().isEmpty()) + result << evt->alias(); + return result; + } + return {}; } QStringList Room::altAliases() const { - return d->getCurrentState()->altAliases(); + return currentState().queryOr(&RoomCanonicalAliasEvent::altAliases, + QStringList()); } QString Room::canonicalAlias() const { - return d->getCurrentState()->alias(); + return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString()); } QString Room::displayName() const { return d->displayname; } QStringList Room::pinnedEventIds() const { - return d->getCurrentState()->pinnedEvents(); + return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList()); } QVector Quotient::Room::pinnedEvents() const { - const auto& pinnedIds = d->getCurrentState()->pinnedEvents(); QVector pinnedEvents; - for (auto&& evtId: pinnedIds) + for (const auto& evtId : pinnedEventIds()) if (const auto& it = findInTimeline(evtId); it != historyEdge()) pinnedEvents.append(it->event()); @@ -582,7 +561,7 @@ void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const { - return d->getCurrentState()->topic(); + return currentState().queryOr(&RoomTopicEvent::topic, QString()); } QString Room::avatarMediaId() const { return d->avatar.mediaId(); } @@ -621,7 +600,8 @@ JoinState Room::memberJoinState(User* user) const Membership Room::memberState(const QString& userId) const { - return d->getCurrentState(userId)->membership(); + return currentState().queryOr(userId, &RoomMemberEvent::membership, + Membership::Leave); } bool Room::isMember(const QString& userId) const @@ -899,8 +879,9 @@ bool Room::canSwitchVersions() const if (!successorId().isEmpty()) return false; // No one can upgrade a room that's already upgraded - if (const auto* plEvt = d->getCurrentState()) { - const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id()); + if (const auto* plEvt = currentState().get()) { + const auto currentUserLevel = + plEvt->powerLevelForUser(localUser()->id()); const auto tombstonePowerLevel = plEvt->powerLevelForState("m.room.tombstone"_ls); return currentUserLevel >= tombstonePowerLevel; @@ -1008,6 +989,16 @@ const Room::RelatedEvents Room::relatedEvents( return relatedEvents(evt.id(), relType); } +const RoomCreateEvent* Room::creation() const +{ + return currentState().get(); +} + +const RoomTombstoneEvent *Room::tombstone() const +{ + return currentState().get(); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -1472,7 +1463,9 @@ int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const { - return !d->getCurrentState()->algorithm().isEmpty(); + return !currentState() + .queryOr(&EncryptionEvent::algorithm, QString()) + .isEmpty(); } const StateEventBase* Room::getCurrentState(const QString& evtType, @@ -1481,13 +1474,7 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } -const QVector -Room::stateEventsOfType(const QString& evtType) const -{ - return d->stateEventsOfType(evtType); -} - -const QHash& Room::currentState() const +RoomStateView Room::currentState() const { return d->currentState; } @@ -1564,7 +1551,7 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) void Room::Private::insertMemberIntoMap(User* u) { const auto maybeUserName = - getCurrentState(u->id())->newDisplayName(); + currentState.query(u->id(), &RoomMemberEvent::newDisplayName); if (!maybeUserName) qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() << "has no name (even empty)"; @@ -1594,9 +1581,9 @@ void Room::Private::insertMemberIntoMap(User* u) void Room::Private::removeMemberFromMap(User* u) { - const auto userName = - getCurrentState( - u->id())->newDisplayName().value_or(QString()); + const auto userName = currentState.queryOr(u->id(), + &RoomMemberEvent::newDisplayName, + QString()); qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName << "for user" << u->id(); @@ -1679,10 +1666,13 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, QString Room::memberName(const QString& mxId) const { // See https://github.com/matrix-org/matrix-doc/issues/1375 - const auto rme = getCurrentState(mxId); - return rme->newDisplayName() ? *rme->newDisplayName() - : rme->prevContent() ? rme->prevContent()->displayName.value_or(QString()) - : QString(); + if (const auto rme = currentState().get(mxId)) { + if (rme->newDisplayName()) + return *rme->newDisplayName(); + if (rme->prevContent() && rme->prevContent()->displayName) + return *rme->prevContent()->displayName; + } + return {}; } QString Room::roomMembername(const User* u) const @@ -1737,10 +1727,13 @@ QString Room::htmlSafeMemberName(const QString& userId) const QUrl Room::memberAvatarUrl(const QString &mxId) const { // See https://github.com/matrix-org/matrix-doc/issues/1375 - const auto rme = getCurrentState(mxId); - return rme->newAvatarUrl() ? *rme->newAvatarUrl() - : rme->prevContent() ? rme->prevContent()->avatarUrl.value_or(QUrl()) - : QUrl(); + if (const auto rme = currentState().get(mxId)) { + if (rme->newAvatarUrl()) + return *rme->newAvatarUrl(); + if (rme->prevContent() && rme->prevContent()->avatarUrl) + return *rme->prevContent()->avatarUrl; + } + return {}; } Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, @@ -2489,12 +2482,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { - const StateEventKey evtKey { oldEvent->matrixType(), - oldEvent->stateKey() }; - Q_ASSERT(currentState.contains(evtKey)); - if (currentState.value(evtKey) == oldEvent.get()) { - Q_ASSERT(ti.index() >= 0); // Historical states can't be in - // currentState + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); qCDebug(STATE).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); @@ -2514,6 +2509,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() return true; } @@ -2754,7 +2750,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) for (const auto& eptr : events) { const auto& e = *eptr; if (e.isStateEvent() - && !currentState.contains({ e.matrixType(), e.stateKey() })) { + && !currentState.contains(e.matrixType(), e.stateKey())) { changes |= q->processStateEvent(e); } } @@ -2791,9 +2787,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return Change::None; // Find a value (create an empty one if necessary) and get a reference - // to it. Can't use getCurrentState<>() because it (creates and) returns - // a stub if a value is not found, and what's needed here is a "real" event - // or nullptr. + // to it, anticipating a change further in the function. auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change // clang-format off -- cgit v1.2.3 From 9ae0d4e45befc79f621b03dc4efe869cd4277e06 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 23 Jan 2022 10:32:40 +0100 Subject: Refactor Room::setState() There are two important aspects here: - Introducing Room::setState(evtType, stateKey, contentJson). These components are ultimately what is getting sent to the homeserver, so it makes sense to expose a respective `setState()` overload. Unlike setState(event) the new overload can be Q_INVOKABLE. - Room::setState() is no more const. Although it doesn't cause any changes in Room class (and only transient changes in Room::Private), it ultimately initiates a change in the room state, so calling it const has always been a bit of hypocrisy. If you relied on that, you most likely do something wrong (see the fix to User::rename() in this very commit for a simple example of such wrongness). Also: the backend is simplified by calling the original templated Room::setState() instead of calling Room::Private::requestSetState() that does exactly the same thing. --- lib/room.cpp | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1450eb3b..abd6110c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -297,7 +297,9 @@ public: QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); - SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) + SetRoomStateWithKeyJob* requestSetState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) { // if (event.roomId().isEmpty()) // event.setRoomId(id); @@ -305,14 +307,8 @@ public: // event.setSender(connection->userId()); // TODO: Queue up state events sending (see #133). // TODO: Maybe addAsPending() as well, despite having no txnId - return connection->callApi( - id, event.matrixType(), event.stateKey(), event.contentJson()); - } - - template - auto requestSetState(ArgTs&&... args) - { - return requestSetState(EvT(std::forward(args)...)); + return connection->callApi(id, evtType, stateKey, + contentJson); } /*! Apply redaction to the timeline @@ -2126,33 +2122,41 @@ QString Room::postJson(const QString& matrixType, return d->sendEvent(loadEvent(matrixType, eventContent)); } -SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const +SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) +{ + return d->requestSetState(evt.matrixType(), evt.stateKey(), + evt.contentJson()); +} + +SetRoomStateWithKeyJob* Room::setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) { - return d->requestSetState(evt); + return d->requestSetState(evtType, stateKey, contentJson); } void Room::setName(const QString& newName) { - d->requestSetState(newName); + setState(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState(newAlias, altAliases()); + setState(newAlias, altAliases()); } void Room::setPinnedEvents(const QStringList& events) { - d->requestSetState(events); + setState(events); } void Room::setLocalAliases(const QStringList& aliases) { - d->requestSetState(canonicalAlias(), aliases); + setState(canonicalAlias(), aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState(newTopic); + setState(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) -- cgit v1.2.3 From 148bb1256fb15c73c605c5301da2b9602f859660 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 11 Feb 2022 23:07:35 +0100 Subject: Implement more suggestions --- lib/room.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a46892f3..492845d7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -370,9 +370,6 @@ public: // A map from (senderKey, sessionId) to InboundGroupSession UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; - void loadMegOlmSessions() { - groupSessions = q->connection()->database()->loadMegolmSessions(q->id(), q->connection()->picklingMode()); - } bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) { @@ -382,14 +379,14 @@ public: return false; } - std::unique_ptr megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); + auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); if (megolmSession->sessionId() != sessionId) { qCWarning(E2EE) << "Session ID mismatch in m.room_key event sent " "from sender with key" << senderKey; return false; } qCWarning(E2EE) << "Adding inbound session"; - q->connection()->database()->saveMegolmSession(q->id(), senderKey, sessionId, megolmSession->pickle(q->connection()->picklingMode())); + connection->saveMegolmSession(q, senderKey, megolmSession.get()); groupSessions[{senderKey, sessionId}] = std::move(megolmSession); return true; } @@ -460,7 +457,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connection->encryptionUpdate(this); } }); - d->loadMegOlmSessions(); + d->groupSessions = connection->loadRoomMegolmSessions(this); connect(this, &Room::beforeDestruction, this, [=](){ connection->database()->clearRoomData(id); -- cgit v1.2.3 From b0aef4af9cbf00755c7b70c71d77f0bf7ce0d200 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 12 Feb 2022 12:12:24 +0100 Subject: Replace QPair with std::pair --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 492845d7..0fc7d23e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -368,7 +368,7 @@ public: #ifdef Quotient_E2EE_ENABLED // A map from (senderKey, sessionId) to InboundGroupSession - UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; + UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) @@ -397,7 +397,7 @@ public: const QString& eventId, QDateTime timestamp) { - const auto senderSessionPairKey = qMakePair(senderKey, sessionId); + const auto senderSessionPairKey = make_pair(senderKey, sessionId); auto groupSessionIt = groupSessions.find(senderSessionPairKey); if (groupSessionIt == groupSessions.end()) { // qCWarning(E2EE) << "Unable to decrypt event" << eventId -- cgit v1.2.3 From eb50b8ebe4447cafe23d5f1d4ca9351e594e917e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 26 Feb 2022 22:22:51 +0100 Subject: Check that decrypted events are for the current room --- lib/room.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ea9915c3..d0ac10f9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1487,7 +1487,13 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) // qCWarning(E2EE) << "Encrypted message is empty"; return {}; } - return encryptedEvent.createDecrypted(decrypted); + auto decryptedEvent = encryptedEvent.createDecrypted(decrypted); + if (decryptedEvent->roomId() == id()) { + return decryptedEvent; + } else { + qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding."; + return nullptr; + } #endif // Quotient_E2EE_ENABLED } -- cgit v1.2.3 From 6e092bd322f52bde1b037d600a7ce71f5b30f2e4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 27 Feb 2022 14:06:27 +0100 Subject: Apply suggestions --- lib/room.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index d0ac10f9..6197b3a2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1490,10 +1490,9 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) auto decryptedEvent = encryptedEvent.createDecrypted(decrypted); if (decryptedEvent->roomId() == id()) { return decryptedEvent; - } else { - qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding."; - return nullptr; } + qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding."; + return {}; #endif // Quotient_E2EE_ENABLED } -- cgit v1.2.3 From fb9d7dc22c74022b914b1964965ed6b8b850d831 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 7 Mar 2022 19:56:10 +0100 Subject: Store the device's ed25519 in the database --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6197b3a2..88aa1d07 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -341,7 +341,7 @@ public: UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; bool addInboundGroupSession(QString senderKey, QString sessionId, - QString sessionKey) + QString sessionKey, QString ed25519Key) { if (groupSessions.find({senderKey, sessionId}) != groupSessions.end()) { qCWarning(E2EE) << "Inbound Megolm session" << sessionId @@ -356,7 +356,7 @@ public: return false; } qCWarning(E2EE) << "Adding inbound session"; - connection->saveMegolmSession(q, senderKey, megolmSession.get()); + connection->saveMegolmSession(q, senderKey, megolmSession.get(), ed25519Key); groupSessions[{senderKey, sessionId}] = std::move(megolmSession); return true; } @@ -1509,7 +1509,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, << roomKeyEvent.algorithm() << "in m.room_key event"; } if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), - roomKeyEvent.sessionKey())) { + roomKeyEvent.sessionKey(), roomKeyEvent.fullJson()["keys"]["ed25519"].toString())) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) { -- cgit v1.2.3 From 658e6db49364862be1ab8e264b03dc04a7bed721 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 2 Mar 2022 00:54:49 +0100 Subject: Implement sending encrypted messages --- lib/room.cpp | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 189 insertions(+), 10 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 88aa1d07..125eae9f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,6 +12,7 @@ #include "avatar.h" #include "connection.h" #include "converters.h" +#include "e2ee/qolmoutboundsession.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -69,6 +70,7 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" +#include "e2ee/qolmutility.h" #include "database.h" #endif // Quotient_E2EE_ENABLED @@ -297,7 +299,8 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - QString doSendEvent(const RoomEvent* pEvent); + //TODO deleteWhenFinishedis ugly, find out if there's something nicer + QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -339,6 +342,10 @@ public: #ifdef Quotient_E2EE_ENABLED // A map from (senderKey, sessionId) to InboundGroupSession UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; + int currentMegolmSessionMessageCount = 0; + //TODO save this to database + unsigned long long currentMegolmSessionCreationTimestamp = 0; + std::unique_ptr currentOutboundMegolmSession = nullptr; bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey, QString ed25519Key) @@ -393,6 +400,144 @@ public: } return content; } + + bool shouldRotateMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentMegolmSessionMessageCount >= rotationMessageCount() || (currentMegolmSessionCreationTimestamp + rotationInterval()) < QDateTime::currentMSecsSinceEpoch(); + } + + bool hasValidMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentOutboundMegolmSession != nullptr; + } + + /// Time in milliseconds after which the outgoing megolmsession should be replaced + unsigned int rotationInterval() const + { + if (!q->usesEncryption()) { + return 0; + } + return q->getCurrentState()->rotationPeriodMs(); + } + + // Number of messages sent by this user after which the outgoing megolm session should be replaced + int rotationMessageCount() const + { + if (!q->usesEncryption()) { + return 0; + } + return q->getCurrentState()->rotationPeriodMsgs(); + } + void createMegolmSession() { + qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); + currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); + currentMegolmSessionMessageCount = 0; + currentMegolmSessionCreationTimestamp = QDateTime::currentMSecsSinceEpoch(); + //TODO store megolm session to database + } + + std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) + { + // Noisy but nice for debugging + //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); + //TODO: store {user->id(), device, sessionId, theirIdentityKey}; required for key requests + const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); + QJsonObject payloadJson = event->fullJson(); + payloadJson["recipient"] = user->id(); + payloadJson["sender"] = connection->user()->id(); + QJsonObject recipientObject; + recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device); + payloadJson["recipient_keys"] = recipientObject; + QJsonObject senderObject; + senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); + payloadJson["keys"] = senderObject; + payloadJson["sender_device"] = connection->deviceId(); + auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted; + encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; + + return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); + } + + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) + { + qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex(); + QHash> hash; + for (const auto& user : q->users()) { + QHash u; + for(const auto &device : connection->devicesForUser(user)) { + if (!connection->hasOlmSession(user, device)) { + u[device] = "signed_curve25519"_ls; + qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; + } + } + if (!u.isEmpty()) { + hash[user->id()] = u; + } + } + auto job = connection->callApi(hash); + connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ + Connection::UsersToDevicesToEvents usersToDevicesToEvents; + auto data = job->jsonData(); + for(const auto &user : q->users()) { + for(const auto &device : connection->devicesForUser(user)) { + const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); + if (!connection->hasOlmSession(user, device)) { + qCDebug(E2EE) << "Creating a new session for" << user << device; + if(data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().isEmpty()) { + qWarning() << "No one time key for" << user << device; + continue; + } + auto keyId = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().keys()[0]; + auto oneTimeKey = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["key"].toString(); + auto signature = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["signatures"].toObject()[user->id()].toObject()[QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject(); + signedData.remove("unsigned"); + signedData.remove("signatures"); + auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); + if (std::holds_alternative(signatureMatch)) { + //TODO i think there are more failed signature checks than expected. Investigate + qDebug() << signedData; + qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; + //Q_ASSERT(false); + continue; + } else { + } + connection->createOlmSession(recipientCurveKey, oneTimeKey); + } + usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); + } + } + connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + }); + } + + //TODO load outbound megolm sessions from database + + void sendMegolmSession() { + // Save the session to this device + const auto sessionId = currentOutboundMegolmSession->sessionId(); + const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); + if(std::holds_alternative(_sessionKey)) { + qCWarning(E2EE) << "Session error"; + //TODO something + } + const auto sessionKey = std::get(_sessionKey); + const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; + + // Send to key to ourself at this device + addInboundGroupSession(senderKey, sessionId, sessionKey); + + // Send the session to other people + sendRoomKeyToDevices(sessionId, sessionKey); + } + #endif // Quotient_E2EE_ENABLED private: @@ -424,9 +569,20 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); + //TODO key at currentIndex to all user devices } }); d->groupSessions = connection->loadRoomMegolmSessions(this); + //TODO load outbound session + connect(this, &Room::userRemoved, this, [this](){ + if (!usesEncryption()) { + return; + } + d->currentOutboundMegolmSession = nullptr; + qCDebug(E2EE) << "Invalidating current megolm session because user left"; + //TODO save old session probably + + }); connect(this, &Room::beforeDestruction, this, [=](){ connection->database()->clearRoomData(id); @@ -1891,19 +2047,39 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { + if (!q->successorId().isEmpty()) { + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; + } if (q->usesEncryption()) { - qCCritical(MAIN) << "Room" << q->objectName() - << "enforces encryption; sending encrypted messages " - "is not supported yet"; + if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { + createMegolmSession(); + sendMegolmSession(); + } + //TODO check if this is necessary + //TODO check if we increment the sent message count + event->setRoomId(id); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + if(std::holds_alternative(encrypted)) { + //TODO something + qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); + return {}; + } + auto encryptedEvent = new EncryptedEvent(std::get(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + encryptedEvent->setTransactionId(connection->generateTxnId()); + encryptedEvent->setRoomId(id); + encryptedEvent->setSender(connection->userId()); + event->setTransactionId(encryptedEvent->transactionId()); + currentMegolmSessionMessageCount++; + // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out + addAsPending(std::move(event)); + return doSendEvent(encryptedEvent, true); } - if (q->successorId().isEmpty()) - return doSendEvent(addAsPending(std::move(event))); - qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; - return {}; + return doSendEvent(addAsPending(std::move(event))); } -QString Room::Private::doSendEvent(const RoomEvent* pEvent) +QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) { const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. @@ -1924,7 +2100,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -1936,6 +2112,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) << "already merged"; emit q->messageSent(txnId, call->eventId()); + if (deleteWhenFinished){ + delete pEvent; + } }); } else onEventSendingFailure(txnId); -- cgit v1.2.3 From 30004d4e0d6aca06491777f77ed966a451eff50f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 2 Mar 2022 23:56:52 +0100 Subject: Save and load outgoing megolm session --- lib/room.cpp | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 125eae9f..e4f6afef 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -342,10 +342,8 @@ public: #ifdef Quotient_E2EE_ENABLED // A map from (senderKey, sessionId) to InboundGroupSession UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; - int currentMegolmSessionMessageCount = 0; - //TODO save this to database - unsigned long long currentMegolmSessionCreationTimestamp = 0; - std::unique_ptr currentOutboundMegolmSession = nullptr; + + QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr; bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey, QString ed25519Key) @@ -376,9 +374,9 @@ public: { auto groupSessionIt = groupSessions.find({ senderKey, sessionId }); if (groupSessionIt == groupSessions.end()) { - // qCWarning(E2EE) << "Unable to decrypt event" << eventId - // << "The sender's device has not sent us the keys for " - // "this message"; + qCWarning(E2EE) << "Unable to decrypt event" << eventId + << "The sender's device has not sent us the keys for " + "this message" << senderKey << sessionId; return QString(); } auto& senderSession = groupSessionIt->second; @@ -406,7 +404,7 @@ public: if (!q->usesEncryption()) { return false; } - return currentMegolmSessionMessageCount >= rotationMessageCount() || (currentMegolmSessionCreationTimestamp + rotationInterval()) < QDateTime::currentMSecsSinceEpoch(); + return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime(); } bool hasValidMegolmSession() const @@ -437,9 +435,7 @@ public: void createMegolmSession() { qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); - currentMegolmSessionMessageCount = 0; - currentMegolmSessionCreationTimestamp = QDateTime::currentMSecsSinceEpoch(); - //TODO store megolm session to database + connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -467,7 +463,7 @@ public: void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) { - qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex(); + qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; for (const auto& user : q->users()) { QHash u; @@ -484,7 +480,7 @@ public: auto job = connection->callApi(hash); connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; - auto data = job->jsonData(); + const auto data = job->jsonData(); for(const auto &user : q->users()) { for(const auto &device : connection->devicesForUser(user)) { const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); @@ -518,8 +514,6 @@ public: }); } - //TODO load outbound megolm sessions from database - void sendMegolmSession() { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); @@ -573,14 +567,16 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) } }); d->groupSessions = connection->loadRoomMegolmSessions(this); - //TODO load outbound session + d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this); + if (d->shouldRotateMegolmSession()) { + d->currentOutboundMegolmSession = nullptr; + } connect(this, &Room::userRemoved, this, [this](){ if (!usesEncryption()) { return; } d->currentOutboundMegolmSession = nullptr; qCDebug(E2EE) << "Invalidating current megolm session because user left"; - //TODO save old session probably }); @@ -2060,6 +2056,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) //TODO check if we increment the sent message count event->setRoomId(id); const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); + connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { //TODO something qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); @@ -2070,7 +2068,6 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); event->setTransactionId(encryptedEvent->transactionId()); - currentMegolmSessionMessageCount++; // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out addAsPending(std::move(event)); return doSendEvent(encryptedEvent, true); -- cgit v1.2.3 From 0ef080bdfa3a8a64d1faadf4a11a8b9dbb5bc055 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 6 Mar 2022 22:54:01 +0100 Subject: Keep log of where we send keys and send keys to new devices and users --- lib/room.cpp | 53 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 19 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e4f6afef..b21bb863 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -436,6 +436,13 @@ public: qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); + + const auto sessionKey = currentOutboundMegolmSession->sessionKey(); + if(std::holds_alternative(sessionKey)) { + qCWarning(E2EE) << "Session error"; + //TODO something + } + addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -461,13 +468,23 @@ public: return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); } - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) + QHash getDevicesWithoutKey() const + { + QHash devices; + auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId())); + for (const auto& user : rawDevices.keys()) { + devices[q->connection()->user(user)] = rawDevices[user]; + } + return devices; + } + + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) { qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; - for (const auto& user : q->users()) { + for (const auto& user : devices.keys()) { QHash u; - for(const auto &device : connection->devicesForUser(user)) { + for(const auto &device : devices[user]) { if (!connection->hasOlmSession(user, device)) { u[device] = "signed_curve25519"_ls; qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; @@ -478,30 +495,28 @@ public: } } auto job = connection->callApi(hash); - connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ + connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; const auto data = job->jsonData(); - for(const auto &user : q->users()) { - for(const auto &device : connection->devicesForUser(user)) { + for(const auto &user : devices.keys()) { + for(const auto &device : devices[user]) { const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); if (!connection->hasOlmSession(user, device)) { qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().isEmpty()) { + if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) { qWarning() << "No one time key for" << user << device; continue; } - auto keyId = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().keys()[0]; - auto oneTimeKey = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["key"].toString(); - auto signature = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["signatures"].toObject()[user->id()].toObject()[QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject(); + const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0]; + const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString(); + const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject(); signedData.remove("unsigned"); signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (std::holds_alternative(signatureMatch)) { //TODO i think there are more failed signature checks than expected. Investigate - qDebug() << signedData; qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; - //Q_ASSERT(false); continue; } else { } @@ -511,10 +526,11 @@ public: } } connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); }); } - void sendMegolmSession() { + void sendMegolmSession(const QHash& devices) { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); @@ -525,11 +541,8 @@ public: const auto sessionKey = std::get(_sessionKey); const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; - // Send to key to ourself at this device - addInboundGroupSession(senderKey, sessionId, sessionKey); - // Send the session to other people - sendRoomKeyToDevices(sessionId, sessionKey); + sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED @@ -2050,8 +2063,10 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); - sendMegolmSession(); } + const auto devicesWithoutKey = getDevicesWithoutKey(); + sendMegolmSession(devicesWithoutKey); + //TODO check if this is necessary //TODO check if we increment the sent message count event->setRoomId(id); -- cgit v1.2.3 From 2071f5020975bc3f5ecbb9e2444acaad8f13060a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 00:06:36 +0100 Subject: Implement sending encrypted files --- lib/room.cpp | 67 +++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 23 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index b21bb863..cbbd9e0e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -299,8 +299,7 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - //TODO deleteWhenFinishedis ugly, find out if there's something nicer - QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); + QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -2060,6 +2059,16 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; return {}; } + + return doSendEvent(addAsPending(std::move(event))); +} + +QString Room::Private::doSendEvent(const RoomEvent* pEvent) +{ + const auto txnId = pEvent->transactionId(); + // TODO, #133: Enqueue the job rather than immediately trigger it. + const RoomEvent* _event = pEvent; + if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); @@ -2067,10 +2076,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if this is necessary //TODO check if we increment the sent message count - event->setRoomId(id); - const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { @@ -2082,23 +2089,14 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); - event->setTransactionId(encryptedEvent->transactionId()); // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out - addAsPending(std::move(event)); - return doSendEvent(encryptedEvent, true); + _event = encryptedEvent; } - return doSendEvent(addAsPending(std::move(event))); -} - -QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) -{ - const auto txnId = pEvent->transactionId(); - // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi(BackgroundRequest, id, - pEvent->matrixType(), txnId, - pEvent->contentJson())) { + _event->matrixType(), txnId, + _event->contentJson())) { Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { @@ -2112,7 +2110,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, _event] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -2124,8 +2122,8 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis << "already merged"; emit q->messageSent(txnId, call->eventId()); - if (deleteWhenFinished){ - delete pEvent; + if (q->usesEncryption()){ + delete _event; } }); } else @@ -2250,13 +2248,16 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) // Below, the upload job is used as a context object to clean up connections const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) { + [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable encryptedFile) { if (tId != txnId) return; const auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { it->setFileUploaded(mxcUri); + if (encryptedFile) { + it->setEncryptedFile(*encryptedFile); + } emit q->pendingEventChanged( int(it - unsyncedEvents.begin())); doSendEvent(it->get()); @@ -2491,6 +2492,20 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); + Omittable encryptedFile = std::nullopt; +#ifdef Quotient_E2EE_ENABLED + QTemporaryFile tempFile; + if (usesEncryption()) { + tempFile.open(); + QFile file(localFilename.toLocalFile()); + file.open(QFile::ReadOnly); + auto [e, data] = EncryptedFile::encryptFile(file.readAll()); + tempFile.write(data); + tempFile.close(); + fileName = QFileInfo(tempFile).absoluteFilePath(); + encryptedFile = e; + } +#endif auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobPending(job)) { d->fileTransfers[id] = { job, fileName, true }; @@ -2499,9 +2514,15 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job] { + connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + if (encryptedFile) { + auto file = *encryptedFile; + file.url = QUrl(job->contentUri()); + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); + } else { + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + } }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); -- cgit v1.2.3 From 7ba17e2b9b6d76aeea250f37c833ad7eed2f61da Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 21:44:10 +0100 Subject: Properly create encrypted edits --- lib/room.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index cbbd9e0e..1762fd5a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2089,6 +2089,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); + if(pEvent->contentJson().contains("m.relates_to"_ls)) { + encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); + } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out _event = encryptedEvent; } -- cgit v1.2.3 From 71eed90fdea8689d237da8de1bf385202b85cffd Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 10 Mar 2022 21:19:49 +0100 Subject: More work; Update olm pickle & timestamps in database; Remove TODOs --- lib/room.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1762fd5a..6d938d4a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -438,8 +438,8 @@ public: const auto sessionKey = currentOutboundMegolmSession->sessionKey(); if(std::holds_alternative(sessionKey)) { - qCWarning(E2EE) << "Session error"; - //TODO something + qCWarning(E2EE) << "Failed to load key for new megolm session"; + return; } addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); } @@ -448,7 +448,6 @@ public: { // Noisy but nice for debugging //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); - //TODO: store {user->id(), device, sessionId, theirIdentityKey}; required for key requests const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); QJsonObject payloadJson = event->fullJson(); payloadJson["recipient"] = user->id(); @@ -493,6 +492,9 @@ public: hash[user->id()] = u; } } + if (hash.isEmpty()) { + return; + } auto job = connection->callApi(hash); connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; @@ -514,7 +516,6 @@ public: signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (std::holds_alternative(signatureMatch)) { - //TODO i think there are more failed signature checks than expected. Investigate qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; continue; } else { @@ -524,8 +525,10 @@ public: usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); } } - connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + if (!usersToDevicesToEvents.empty()) { + connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + } }); } @@ -534,8 +537,8 @@ public: const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); if(std::holds_alternative(_sessionKey)) { - qCWarning(E2EE) << "Session error"; - //TODO something + qCWarning(E2EE) << "Error loading session key"; + return; } const auto sessionKey = std::get(_sessionKey); const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; @@ -575,7 +578,6 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); - //TODO key at currentIndex to all user devices } }); d->groupSessions = connection->loadRoomMegolmSessions(this); @@ -2070,18 +2072,20 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const RoomEvent* _event = pEvent; if (q->usesEncryption()) { +#ifndef Quotient_E2EE_ENABLED + qWarning() << "This build of libQuotient does not support E2EE."; + return {}; +#else if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); } const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if we increment the sent message count const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { - //TODO something qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); return {}; } @@ -2094,6 +2098,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out _event = encryptedEvent; +#endif } if (auto call = -- cgit v1.2.3 From deb1cb4dbbbd4f607e05e9a3b624d3f5f7e2772e Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 10 Mar 2022 21:51:42 +0100 Subject: Update lib/room.cpp --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6d938d4a..5d702fba 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -373,9 +373,9 @@ public: { auto groupSessionIt = groupSessions.find({ senderKey, sessionId }); if (groupSessionIt == groupSessions.end()) { - qCWarning(E2EE) << "Unable to decrypt event" << eventId - << "The sender's device has not sent us the keys for " - "this message" << senderKey << sessionId; + // qCWarning(E2EE) << "Unable to decrypt event" << eventId + // << "The sender's device has not sent us the keys for " + // "this message" << senderKey << sessionId; return QString(); } auto& senderSession = groupSessionIt->second; -- cgit v1.2.3 From 336f8b2e93b43e85b9ea4b9da2b2b34221e657aa Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 10 Mar 2022 21:53:14 +0100 Subject: Minor fix --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 5d702fba..a1c73666 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -374,8 +374,8 @@ public: auto groupSessionIt = groupSessions.find({ senderKey, sessionId }); if (groupSessionIt == groupSessions.end()) { // qCWarning(E2EE) << "Unable to decrypt event" << eventId - // << "The sender's device has not sent us the keys for " - // "this message" << senderKey << sessionId; + // << "The sender's device has not sent us the keys for " + // "this message"; return QString(); } auto& senderSession = groupSessionIt->second; -- cgit v1.2.3 From b0a43c3534865b9fcc1af90ee2c821ac11b2a204 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 9 Apr 2022 02:00:52 +0200 Subject: Don't crash when decrypting existing messages --- lib/room.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 88aa1d07..3ba6890a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1512,7 +1512,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, roomKeyEvent.sessionKey(), roomKeyEvent.fullJson()["keys"]["ed25519"].toString())) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); - for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) { + auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()]; + for (const auto& eventId : undecryptedEvents) { const auto pIdx = d->eventsIndex.constFind(eventId); if (pIdx == d->eventsIndex.cend()) continue; @@ -1524,7 +1525,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, auto& decryptedEvent = *decrypted; auto oldEvent = ti.replaceEvent(std::move(decrypted)); decryptedEvent.setOriginalEvent(std::move(oldEvent)); - emit replacedEvent(ti.event(), decrypted->originalEvent()); + emit replacedEvent(ti.event(), decryptedEvent.originalEvent()); d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; } } -- cgit v1.2.3 From 2af8d83526ed7a24c18b185e1d64d97632e10f1e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 9 Apr 2022 02:04:39 +0200 Subject: Prepare for MSC 3700 --- lib/room.cpp | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 3ba6890a..041d88c8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -337,27 +337,25 @@ public: bool isLocalUser(const User* u) const { return u == q->localUser(); } #ifdef Quotient_E2EE_ENABLED - // A map from (senderKey, sessionId) to InboundGroupSession - UnorderedMap, QOlmInboundGroupSessionPtr> groupSessions; + UnorderedMap groupSessions; - bool addInboundGroupSession(QString senderKey, QString sessionId, - QString sessionKey, QString ed25519Key) + bool addInboundGroupSession(QString sessionId, QString sessionKey, const QString& senderId, const QString& olmSessionId) { - if (groupSessions.find({senderKey, sessionId}) != groupSessions.end()) { - qCWarning(E2EE) << "Inbound Megolm session" << sessionId - << "with senderKey" << senderKey << "already exists"; + if (groupSessions.find(sessionId) != groupSessions.end()) { + qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; return false; } auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); if (megolmSession->sessionId() != sessionId) { - qCWarning(E2EE) << "Session ID mismatch in m.room_key event sent " - "from sender with key" << senderKey; + qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; return false; } + megolmSession->setSenderId(senderId); + megolmSession->setOlmSessionId(olmSessionId); qCWarning(E2EE) << "Adding inbound session"; - connection->saveMegolmSession(q, senderKey, megolmSession.get(), ed25519Key); - groupSessions[{senderKey, sessionId}] = std::move(megolmSession); + connection->saveMegolmSession(q, megolmSession.get()); + groupSessions[sessionId] = std::move(megolmSession); return true; } @@ -365,9 +363,10 @@ public: const QString& senderKey, const QString& sessionId, const QString& eventId, - QDateTime timestamp) + QDateTime timestamp, + const QString& senderId) { - auto groupSessionIt = groupSessions.find({ senderKey, sessionId }); + auto groupSessionIt = groupSessions.find(sessionId); if (groupSessionIt == groupSessions.end()) { // qCWarning(E2EE) << "Unable to decrypt event" << eventId // << "The sender's device has not sent us the keys for " @@ -375,6 +374,10 @@ public: return QString(); } auto& senderSession = groupSessionIt->second; + if (senderSession->senderId() != senderId) { + qCWarning(E2EE) << "Sender from event does not match sender from session"; + return {}; + } auto decryptResult = senderSession->decrypt(cipher); if(std::holds_alternative(decryptResult)) { qCWarning(E2EE) << "Unable to decrypt event" << eventId @@ -1482,9 +1485,9 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) QString decrypted = d->groupSessionDecryptMessage( encryptedEvent.ciphertext(), encryptedEvent.senderKey(), encryptedEvent.sessionId(), encryptedEvent.id(), - encryptedEvent.originTimestamp()); + encryptedEvent.originTimestamp(), encryptedEvent.senderId()); if (decrypted.isEmpty()) { - // qCWarning(E2EE) << "Encrypted message is empty"; + qCWarning(E2EE) << "Encrypted message is empty"; return {}; } auto decryptedEvent = encryptedEvent.createDecrypted(decrypted); @@ -1497,19 +1500,21 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) } void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, - const QString& senderKey) + const QString& senderId, + const QString& olmSessionId) { #ifndef Quotient_E2EE_ENABLED Q_UNUSED(roomKeyEvent) - Q_UNUSED(senderKey) + Q_UNUSED(senderId) + Q_UNUSED(olmSessionId) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { qCWarning(E2EE) << "Ignoring unsupported algorithm" << roomKeyEvent.algorithm() << "in m.room_key event"; } - if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), - roomKeyEvent.sessionKey(), roomKeyEvent.fullJson()["keys"]["ed25519"].toString())) { + if (d->addInboundGroupSession(roomKeyEvent.sessionId(), + roomKeyEvent.sessionKey(), senderId, olmSessionId)) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()]; -- cgit v1.2.3 From b7dbd566f0611a88aafef78f23d53b724302c33f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 9 Apr 2022 18:34:19 +0200 Subject: Comment out debug statement --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 041d88c8..183e242a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1487,7 +1487,7 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) encryptedEvent.sessionId(), encryptedEvent.id(), encryptedEvent.originTimestamp(), encryptedEvent.senderId()); if (decrypted.isEmpty()) { - qCWarning(E2EE) << "Encrypted message is empty"; + // qCWarning(E2EE) << "Encrypted message is empty"; return {}; } auto decryptedEvent = encryptedEvent.createDecrypted(decrypted); -- cgit v1.2.3 From 3a81aea545ec014d597f3756c362913aad4421df Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Apr 2022 23:32:59 +0200 Subject: Fixes --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 61f57245..a423e04f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -444,7 +444,7 @@ public: qCWarning(E2EE) << "Failed to load key for new megolm session"; return; } - addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get(sessionKey), q->localUser()->id(), "SELF"_ls); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) -- cgit v1.2.3 From f4a20cc3710ee8f4b1788f73d05466aa0e660d61 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 May 2022 19:02:35 +0200 Subject: More cleanup --- lib/room.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 183e242a..4d9f952c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -3064,15 +3064,16 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) QElapsedTimer et; et.start(); if (auto* evt = eventCast(event)) { + const auto& users = evt->users(); d->usersTyping.clear(); - d->usersTyping.reserve(evt->users().size()); // Assume all are members - for (const auto& userId : evt->users()) + d->usersTyping.reserve(users.size()); // Assume all are members + for (const auto& userId : users) if (isMember(userId)) d->usersTyping.append(user(userId)); - if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) + if (users.size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) - << "Processing typing events from" << evt->users().size() + << "Processing typing events from" << users.size() << "user(s) in" << objectName() << "took" << et; emit typingChanged(); } -- cgit v1.2.3 From 98fdf62391fdc5135d8324476903a4c43345e732 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 6 May 2022 22:39:34 +0200 Subject: Fix race condition in consumeRoomData() QCoreApplication::processEvents() is well-known to be a _wrong_ solution to the unresponsive UI problem; despite that, connection.cpp has long had that call to let UI update itself while processing bulky room updates (mainly from the initial sync). This commit finally fixes this, after an (admittedly rare) race condition has been hit, as follows: 0. Pre-requisite: quotest runs all the tests and is about to leave the room; there's an ongoing sync request. 1. Quotest calls /leave 2. Sync returns, with the batch of _several_ rooms (that's important) 3. The above code handles the first room in the batch 4. processEvents() is called, just in time for the /leave response. 5. The /leave response handler in quotest ends up calling Connection::logout() (processEvents() still hasn't returned). 6. Connection::logout() calls abandon() on the ongoing SyncJob, pulling the rug from under onSyncSuccess()/consumeRoomData(). 7. processEvents() returns and the above code proceeds to the next room - only to find that the roomDataList (that is a ref to a structure owned by SyncJob), is now pointing to garbage. Morals of the story: 1. processEvents() effectively makes code multi-threaded: one flow is suspended and another one may run _on the same data_. After the first flow is resumed, it cannot make any assumptions regarding which data the second flow touched and/or changed. 2. The library had quite a few cases of using &&-refs, avoiding even move operations but also leaving ownership of the data with the original producer (SyncJob). If the lifetime of that producer ends too soon, those refs become dangling. The fix makes two important things, respectively: 2. Ownership of room data is now transfered to the processing side, the moment it is scheduled (see below), in the form of moving into a lambda capture. 1. Instead of processEvents(), processing of room data is scheduled via QMetaObject::invokeMethod(), uncoupling the moment when the data was received in SyncJob from the moment they are processed in Room::updateData() (and all the numerous signal-slots it calls). Also: Room::baseStateLoaded now causes Connection::loadedRoomState, not the other way round - this is more natural and doesn't need Connection to keep firstTimeRooms map around. --- lib/room.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4d9f952c..4ba699b0 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -415,11 +415,6 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name - connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) { - if (this == r) - emit baseStateLoaded(); - return this == r; // loadedRoomState fires only once per room - }); #ifdef Quotient_E2EE_ENABLED connectSingleShot(this, &Room::encryption, this, [this, connection](){ connection->encryptionUpdate(this); @@ -1820,6 +1815,9 @@ Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, void Room::updateData(SyncRoomData&& data, bool fromCache) { + qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName(); + bool firstUpdate = d->baseState.empty(); + if (d->prevBatch.isEmpty()) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); @@ -1845,6 +1843,9 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) emit namesChanged(this); d->postprocessChanges(roomChanges, !fromCache); + if (firstUpdate) + emit baseStateLoaded(); + qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName(); } void Room::Private::postprocessChanges(Changes changes, bool saveState) -- cgit v1.2.3 From b32c1c27ae412d073a7e98bdaf22678bdc66ab23 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 10 May 2022 22:56:02 +0200 Subject: CallAnswerEvent: drop lifetime See https://github.com/matrix-org/matrix-spec/pull/1054. # Conflicts: # lib/events/callanswerevent.cpp # lib/events/callanswerevent.h --- lib/room.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4ba699b0..993455be 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2232,11 +2232,12 @@ void Room::sendCallCandidates(const QString& callId, d->sendEvent(callId, candidates); } -void Room::answerCall(const QString& callId, const int lifetime, +void Room::answerCall(const QString& callId, [[maybe_unused]] int lifetime, const QString& sdp) { - Q_ASSERT(supportsCalls()); - d->sendEvent(callId, lifetime, sdp); + qCWarning(MAIN) << "To client developer: drop lifetime parameter from " + "Room::answerCall(), it is no more accepted"; + answerCall(callId, sdp); } void Room::answerCall(const QString& callId, const QString& sdp) -- cgit v1.2.3 From 572b727b22d66a79431326c924236ef431fd972b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 14 May 2022 11:20:43 +0200 Subject: Cleanup across the board Mainly driven by clang-tidy and SonarCloud warnings (sadly, SonarCloud doesn't store historical reports so no link can be provided here). --- lib/room.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 993455be..0a997b9c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -339,14 +339,16 @@ public: #ifdef Quotient_E2EE_ENABLED UnorderedMap groupSessions; - bool addInboundGroupSession(QString sessionId, QString sessionKey, const QString& senderId, const QString& olmSessionId) + bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, + const QString& senderId, + const QString& olmSessionId) { - if (groupSessions.find(sessionId) != groupSessions.end()) { + if (groupSessions.contains(sessionId)) { qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; return false; } - auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); + auto megolmSession = QOlmInboundGroupSession::create(sessionKey); if (megolmSession->sessionId() != sessionId) { qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; return false; @@ -354,13 +356,12 @@ public: megolmSession->setSenderId(senderId); megolmSession->setOlmSessionId(olmSessionId); qCWarning(E2EE) << "Adding inbound session"; - connection->saveMegolmSession(q, megolmSession.get()); + connection->saveMegolmSession(q, *megolmSession); groupSessions[sessionId] = std::move(megolmSession); return true; } QString groupSessionDecryptMessage(QByteArray cipher, - const QString& senderKey, const QString& sessionId, const QString& eventId, QDateTime timestamp, @@ -1478,9 +1479,9 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) return {}; } QString decrypted = d->groupSessionDecryptMessage( - encryptedEvent.ciphertext(), encryptedEvent.senderKey(), - encryptedEvent.sessionId(), encryptedEvent.id(), - encryptedEvent.originTimestamp(), encryptedEvent.senderId()); + encryptedEvent.ciphertext(), encryptedEvent.sessionId(), + encryptedEvent.id(), encryptedEvent.originTimestamp(), + encryptedEvent.senderId()); if (decrypted.isEmpty()) { // qCWarning(E2EE) << "Encrypted message is empty"; return {}; @@ -1509,7 +1510,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, << roomKeyEvent.algorithm() << "in m.room_key event"; } if (d->addInboundGroupSession(roomKeyEvent.sessionId(), - roomKeyEvent.sessionKey(), senderId, olmSessionId)) { + roomKeyEvent.sessionKey(), senderId, + olmSessionId)) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()]; -- cgit v1.2.3 From 0599ef6e603dce219b2ba000d7322e8d937753b9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 14 May 2022 20:13:00 +0200 Subject: room.cpp: use return {} where appropriate --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 0a997b9c..db49e80f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -372,7 +372,7 @@ public: // qCWarning(E2EE) << "Unable to decrypt event" << eventId // << "The sender's device has not sent us the keys for " // "this message"; - return QString(); + return {}; } auto& senderSession = groupSessionIt->second; if (senderSession->senderId() != senderId) { @@ -383,7 +383,7 @@ public: if(std::holds_alternative(decryptResult)) { qCWarning(E2EE) << "Unable to decrypt event" << eventId << "with matching megolm session:" << std::get(decryptResult); - return QString(); + return {}; } const auto& [content, index] = std::get>(decryptResult); const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index); @@ -392,7 +392,7 @@ public: } else { if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) { qCWarning(E2EE) << "Detected a replay attack on event" << eventId; - return QString(); + return {}; } } return content; -- cgit v1.2.3 From 79b3dba1ed4b6870c4e989ada88e33b1ce0ddc21 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 16 May 2022 10:41:54 +0200 Subject: QOlmExpected and associated refactoring As mentioned in the commit introducing `Expected`, `QOlmExpected` is simply an alias for `Expected`. This simplifies quite a few function signatures in `QOlm*` classes and collapses unwieldy `std::holds_alternative<>`/`std::get<>` constructs into a neat contextual bool cast and an invocation of `operator*` or `value()`/`error()` accessors that don't need to specify the type. While refactoring the code, I found a couple of cases of mismatching `uint32_t` and `qint32_t` in return values; a couple of cases where `decrypt()` returns `QString` which is in fact `QByteArray` (e.g., in `QOlmSession::decrypt()`); there's a repetitive algorithm in `Connection::Private::sessionDecryptPrekey()` and `sessionDecryptGeneral()` --- lib/room.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index db49e80f..1314803e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -380,17 +380,22 @@ public: return {}; } auto decryptResult = senderSession->decrypt(cipher); - if(std::holds_alternative(decryptResult)) { + if(!decryptResult) { qCWarning(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << std::get(decryptResult); + << "with matching megolm session:" << decryptResult.error(); return {}; } - const auto& [content, index] = std::get>(decryptResult); - const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index); + const auto& [content, index] = *decryptResult; + const auto& [recordEventId, ts] = + q->connection()->database()->groupSessionIndexRecord( + q->id(), senderSession->sessionId(), index); if (recordEventId.isEmpty()) { - q->connection()->database()->addGroupSessionIndexRecord(q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch()); + q->connection()->database()->addGroupSessionIndexRecord( + q->id(), senderSession->sessionId(), index, eventId, + timestamp.toMSecsSinceEpoch()); } else { - if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) { + if ((eventId != recordEventId) + || (ts != timestamp.toMSecsSinceEpoch())) { qCWarning(E2EE) << "Detected a replay attack on event" << eventId; return {}; } -- cgit v1.2.3 From 20bd96ac67c06617e619460c4cd07b5e15cc74d7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 2 Mar 2022 00:54:49 +0100 Subject: Implement sending encrypted messages --- lib/room.cpp | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 189 insertions(+), 10 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1314803e..e8b63235 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,6 +12,7 @@ #include "avatar.h" #include "connection.h" #include "converters.h" +#include "e2ee/qolmoutboundsession.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -69,6 +70,7 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" +#include "e2ee/qolmutility.h" #include "database.h" #endif // Quotient_E2EE_ENABLED @@ -297,7 +299,8 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - QString doSendEvent(const RoomEvent* pEvent); + //TODO deleteWhenFinishedis ugly, find out if there's something nicer + QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -338,6 +341,10 @@ public: #ifdef Quotient_E2EE_ENABLED UnorderedMap groupSessions; + int currentMegolmSessionMessageCount = 0; + //TODO save this to database + unsigned long long currentMegolmSessionCreationTimestamp = 0; + std::unique_ptr currentOutboundMegolmSession = nullptr; bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, const QString& senderId, @@ -402,6 +409,144 @@ public: } return content; } + + bool shouldRotateMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentMegolmSessionMessageCount >= rotationMessageCount() || (currentMegolmSessionCreationTimestamp + rotationInterval()) < QDateTime::currentMSecsSinceEpoch(); + } + + bool hasValidMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentOutboundMegolmSession != nullptr; + } + + /// Time in milliseconds after which the outgoing megolmsession should be replaced + unsigned int rotationInterval() const + { + if (!q->usesEncryption()) { + return 0; + } + return q->getCurrentState()->rotationPeriodMs(); + } + + // Number of messages sent by this user after which the outgoing megolm session should be replaced + int rotationMessageCount() const + { + if (!q->usesEncryption()) { + return 0; + } + return q->getCurrentState()->rotationPeriodMsgs(); + } + void createMegolmSession() { + qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); + currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); + currentMegolmSessionMessageCount = 0; + currentMegolmSessionCreationTimestamp = QDateTime::currentMSecsSinceEpoch(); + //TODO store megolm session to database + } + + std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) + { + // Noisy but nice for debugging + //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); + //TODO: store {user->id(), device, sessionId, theirIdentityKey}; required for key requests + const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); + QJsonObject payloadJson = event->fullJson(); + payloadJson["recipient"] = user->id(); + payloadJson["sender"] = connection->user()->id(); + QJsonObject recipientObject; + recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device); + payloadJson["recipient_keys"] = recipientObject; + QJsonObject senderObject; + senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); + payloadJson["keys"] = senderObject; + payloadJson["sender_device"] = connection->deviceId(); + auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted; + encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; + + return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); + } + + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) + { + qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex(); + QHash> hash; + for (const auto& user : q->users()) { + QHash u; + for(const auto &device : connection->devicesForUser(user)) { + if (!connection->hasOlmSession(user, device)) { + u[device] = "signed_curve25519"_ls; + qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; + } + } + if (!u.isEmpty()) { + hash[user->id()] = u; + } + } + auto job = connection->callApi(hash); + connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ + Connection::UsersToDevicesToEvents usersToDevicesToEvents; + auto data = job->jsonData(); + for(const auto &user : q->users()) { + for(const auto &device : connection->devicesForUser(user)) { + const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); + if (!connection->hasOlmSession(user, device)) { + qCDebug(E2EE) << "Creating a new session for" << user << device; + if(data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().isEmpty()) { + qWarning() << "No one time key for" << user << device; + continue; + } + auto keyId = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().keys()[0]; + auto oneTimeKey = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["key"].toString(); + auto signature = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["signatures"].toObject()[user->id()].toObject()[QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject(); + signedData.remove("unsigned"); + signedData.remove("signatures"); + auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); + if (std::holds_alternative(signatureMatch)) { + //TODO i think there are more failed signature checks than expected. Investigate + qDebug() << signedData; + qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; + //Q_ASSERT(false); + continue; + } else { + } + connection->createOlmSession(recipientCurveKey, oneTimeKey); + } + usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); + } + } + connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + }); + } + + //TODO load outbound megolm sessions from database + + void sendMegolmSession() { + // Save the session to this device + const auto sessionId = currentOutboundMegolmSession->sessionId(); + const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); + if(std::holds_alternative(_sessionKey)) { + qCWarning(E2EE) << "Session error"; + //TODO something + } + const auto sessionKey = std::get(_sessionKey); + const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; + + // Send to key to ourself at this device + addInboundGroupSession(senderKey, sessionId, sessionKey); + + // Send the session to other people + sendRoomKeyToDevices(sessionId, sessionKey); + } + #endif // Quotient_E2EE_ENABLED private: @@ -428,9 +573,20 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); + //TODO key at currentIndex to all user devices } }); d->groupSessions = connection->loadRoomMegolmSessions(this); + //TODO load outbound session + connect(this, &Room::userRemoved, this, [this](){ + if (!usesEncryption()) { + return; + } + d->currentOutboundMegolmSession = nullptr; + qCDebug(E2EE) << "Invalidating current megolm session because user left"; + //TODO save old session probably + + }); connect(this, &Room::beforeDestruction, this, [=](){ connection->database()->clearRoomData(id); @@ -1905,19 +2061,39 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { + if (!q->successorId().isEmpty()) { + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; + } if (q->usesEncryption()) { - qCCritical(MAIN) << "Room" << q->objectName() - << "enforces encryption; sending encrypted messages " - "is not supported yet"; + if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { + createMegolmSession(); + sendMegolmSession(); + } + //TODO check if this is necessary + //TODO check if we increment the sent message count + event->setRoomId(id); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + if(std::holds_alternative(encrypted)) { + //TODO something + qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); + return {}; + } + auto encryptedEvent = new EncryptedEvent(std::get(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + encryptedEvent->setTransactionId(connection->generateTxnId()); + encryptedEvent->setRoomId(id); + encryptedEvent->setSender(connection->userId()); + event->setTransactionId(encryptedEvent->transactionId()); + currentMegolmSessionMessageCount++; + // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out + addAsPending(std::move(event)); + return doSendEvent(encryptedEvent, true); } - if (q->successorId().isEmpty()) - return doSendEvent(addAsPending(std::move(event))); - qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; - return {}; + return doSendEvent(addAsPending(std::move(event))); } -QString Room::Private::doSendEvent(const RoomEvent* pEvent) +QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) { const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. @@ -1938,7 +2114,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -1950,6 +2126,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) << "already merged"; emit q->messageSent(txnId, call->eventId()); + if (deleteWhenFinished){ + delete pEvent; + } }); } else onEventSendingFailure(txnId); -- cgit v1.2.3 From 3eb7ad8b0a1ac0f6f9cda679108937a01268f184 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 16 May 2022 20:46:34 +0200 Subject: Save and load outgoing megolm session --- lib/room.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e8b63235..9e2bd7dd 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -344,7 +344,7 @@ public: int currentMegolmSessionMessageCount = 0; //TODO save this to database unsigned long long currentMegolmSessionCreationTimestamp = 0; - std::unique_ptr currentOutboundMegolmSession = nullptr; + QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr; bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, const QString& senderId, @@ -415,7 +415,7 @@ public: if (!q->usesEncryption()) { return false; } - return currentMegolmSessionMessageCount >= rotationMessageCount() || (currentMegolmSessionCreationTimestamp + rotationInterval()) < QDateTime::currentMSecsSinceEpoch(); + return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime(); } bool hasValidMegolmSession() const @@ -446,9 +446,7 @@ public: void createMegolmSession() { qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); - currentMegolmSessionMessageCount = 0; - currentMegolmSessionCreationTimestamp = QDateTime::currentMSecsSinceEpoch(); - //TODO store megolm session to database + connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -476,7 +474,7 @@ public: void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) { - qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex(); + qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; for (const auto& user : q->users()) { QHash u; @@ -493,7 +491,7 @@ public: auto job = connection->callApi(hash); connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; - auto data = job->jsonData(); + const auto data = job->jsonData(); for(const auto &user : q->users()) { for(const auto &device : connection->devicesForUser(user)) { const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); @@ -527,8 +525,6 @@ public: }); } - //TODO load outbound megolm sessions from database - void sendMegolmSession() { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); @@ -577,14 +573,16 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) } }); d->groupSessions = connection->loadRoomMegolmSessions(this); - //TODO load outbound session + d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this); + if (d->shouldRotateMegolmSession()) { + d->currentOutboundMegolmSession = nullptr; + } connect(this, &Room::userRemoved, this, [this](){ if (!usesEncryption()) { return; } d->currentOutboundMegolmSession = nullptr; qCDebug(E2EE) << "Invalidating current megolm session because user left"; - //TODO save old session probably }); @@ -2074,6 +2072,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) //TODO check if we increment the sent message count event->setRoomId(id); const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); + connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { //TODO something qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); @@ -2084,7 +2084,6 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); event->setTransactionId(encryptedEvent->transactionId()); - currentMegolmSessionMessageCount++; // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out addAsPending(std::move(event)); return doSendEvent(encryptedEvent, true); -- cgit v1.2.3 From 6f5ac9b7315d75692960e5eac7b1eb6867c0d203 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 6 Mar 2022 22:54:01 +0100 Subject: Keep log of where we send keys and send keys to new devices and users --- lib/room.cpp | 53 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 19 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 9e2bd7dd..7db9f8e9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -447,6 +447,13 @@ public: qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); + + const auto sessionKey = currentOutboundMegolmSession->sessionKey(); + if(std::holds_alternative(sessionKey)) { + qCWarning(E2EE) << "Session error"; + //TODO something + } + addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -472,13 +479,23 @@ public: return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); } - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) + QHash getDevicesWithoutKey() const + { + QHash devices; + auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId())); + for (const auto& user : rawDevices.keys()) { + devices[q->connection()->user(user)] = rawDevices[user]; + } + return devices; + } + + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) { qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; - for (const auto& user : q->users()) { + for (const auto& user : devices.keys()) { QHash u; - for(const auto &device : connection->devicesForUser(user)) { + for(const auto &device : devices[user]) { if (!connection->hasOlmSession(user, device)) { u[device] = "signed_curve25519"_ls; qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; @@ -489,30 +506,28 @@ public: } } auto job = connection->callApi(hash); - connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ + connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; const auto data = job->jsonData(); - for(const auto &user : q->users()) { - for(const auto &device : connection->devicesForUser(user)) { + for(const auto &user : devices.keys()) { + for(const auto &device : devices[user]) { const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); if (!connection->hasOlmSession(user, device)) { qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().isEmpty()) { + if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) { qWarning() << "No one time key for" << user << device; continue; } - auto keyId = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().keys()[0]; - auto oneTimeKey = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["key"].toString(); - auto signature = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["signatures"].toObject()[user->id()].toObject()[QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject(); + const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0]; + const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString(); + const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject(); signedData.remove("unsigned"); signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (std::holds_alternative(signatureMatch)) { //TODO i think there are more failed signature checks than expected. Investigate - qDebug() << signedData; qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; - //Q_ASSERT(false); continue; } else { } @@ -522,10 +537,11 @@ public: } } connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); }); } - void sendMegolmSession() { + void sendMegolmSession(const QHash& devices) { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); @@ -536,11 +552,8 @@ public: const auto sessionKey = std::get(_sessionKey); const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; - // Send to key to ourself at this device - addInboundGroupSession(senderKey, sessionId, sessionKey); - // Send the session to other people - sendRoomKeyToDevices(sessionId, sessionKey); + sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED @@ -2066,8 +2079,10 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); - sendMegolmSession(); } + const auto devicesWithoutKey = getDevicesWithoutKey(); + sendMegolmSession(devicesWithoutKey); + //TODO check if this is necessary //TODO check if we increment the sent message count event->setRoomId(id); -- cgit v1.2.3 From efa450920e5fc338e771e653ca0889e948d04ee7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 00:06:36 +0100 Subject: Implement sending encrypted files --- lib/room.cpp | 67 +++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 23 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7db9f8e9..763ca31c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -299,8 +299,7 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - //TODO deleteWhenFinishedis ugly, find out if there's something nicer - QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); + QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -2076,6 +2075,16 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; return {}; } + + return doSendEvent(addAsPending(std::move(event))); +} + +QString Room::Private::doSendEvent(const RoomEvent* pEvent) +{ + const auto txnId = pEvent->transactionId(); + // TODO, #133: Enqueue the job rather than immediately trigger it. + const RoomEvent* _event = pEvent; + if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); @@ -2083,10 +2092,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if this is necessary //TODO check if we increment the sent message count - event->setRoomId(id); - const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { @@ -2098,23 +2105,14 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); - event->setTransactionId(encryptedEvent->transactionId()); // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out - addAsPending(std::move(event)); - return doSendEvent(encryptedEvent, true); + _event = encryptedEvent; } - return doSendEvent(addAsPending(std::move(event))); -} - -QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) -{ - const auto txnId = pEvent->transactionId(); - // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi(BackgroundRequest, id, - pEvent->matrixType(), txnId, - pEvent->contentJson())) { + _event->matrixType(), txnId, + _event->contentJson())) { Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { @@ -2128,7 +2126,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, _event] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -2140,8 +2138,8 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis << "already merged"; emit q->messageSent(txnId, call->eventId()); - if (deleteWhenFinished){ - delete pEvent; + if (q->usesEncryption()){ + delete _event; } }); } else @@ -2266,13 +2264,16 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) // Below, the upload job is used as a context object to clean up connections const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) { + [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable encryptedFile) { if (tId != txnId) return; const auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { it->setFileUploaded(mxcUri); + if (encryptedFile) { + it->setEncryptedFile(*encryptedFile); + } emit q->pendingEventChanged( int(it - unsyncedEvents.begin())); doSendEvent(it->get()); @@ -2508,6 +2509,20 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); + Omittable encryptedFile = std::nullopt; +#ifdef Quotient_E2EE_ENABLED + QTemporaryFile tempFile; + if (usesEncryption()) { + tempFile.open(); + QFile file(localFilename.toLocalFile()); + file.open(QFile::ReadOnly); + auto [e, data] = EncryptedFile::encryptFile(file.readAll()); + tempFile.write(data); + tempFile.close(); + fileName = QFileInfo(tempFile).absoluteFilePath(); + encryptedFile = e; + } +#endif auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobPending(job)) { d->fileTransfers[id] = { job, fileName, true }; @@ -2516,9 +2531,15 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job] { + connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + if (encryptedFile) { + auto file = *encryptedFile; + file.url = QUrl(job->contentUri()); + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); + } else { + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + } }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); -- cgit v1.2.3 From fcde81c8618fbe10c1cb91c0ec6887a3df705a23 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 21:44:10 +0100 Subject: Properly create encrypted edits --- lib/room.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 763ca31c..a42b7184 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2105,6 +2105,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); + if(pEvent->contentJson().contains("m.relates_to"_ls)) { + encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); + } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out _event = encryptedEvent; } -- cgit v1.2.3 From e437c29654e8f988ad694083401bbef23fbbcb18 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 16 May 2022 20:51:41 +0200 Subject: More work; Update olm pickle & timestamps in database; Remove TODOs --- lib/room.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a42b7184..0ca8f648 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -449,8 +449,8 @@ public: const auto sessionKey = currentOutboundMegolmSession->sessionKey(); if(std::holds_alternative(sessionKey)) { - qCWarning(E2EE) << "Session error"; - //TODO something + qCWarning(E2EE) << "Failed to load key for new megolm session"; + return; } addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); } @@ -459,7 +459,6 @@ public: { // Noisy but nice for debugging //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); - //TODO: store {user->id(), device, sessionId, theirIdentityKey}; required for key requests const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); QJsonObject payloadJson = event->fullJson(); payloadJson["recipient"] = user->id(); @@ -504,6 +503,9 @@ public: hash[user->id()] = u; } } + if (hash.isEmpty()) { + return; + } auto job = connection->callApi(hash); connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; @@ -525,7 +527,6 @@ public: signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (std::holds_alternative(signatureMatch)) { - //TODO i think there are more failed signature checks than expected. Investigate qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; continue; } else { @@ -535,8 +536,10 @@ public: usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); } } - connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + if (!usersToDevicesToEvents.empty()) { + connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + } }); } @@ -545,8 +548,8 @@ public: const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); if(std::holds_alternative(_sessionKey)) { - qCWarning(E2EE) << "Session error"; - //TODO something + qCWarning(E2EE) << "Error loading session key"; + return; } const auto sessionKey = std::get(_sessionKey); const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; @@ -581,7 +584,6 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); - //TODO key at currentIndex to all user devices } }); d->groupSessions = connection->loadRoomMegolmSessions(this); @@ -2086,18 +2088,20 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const RoomEvent* _event = pEvent; if (q->usesEncryption()) { +#ifndef Quotient_E2EE_ENABLED + qWarning() << "This build of libQuotient does not support E2EE."; + return {}; +#else if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); } const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if we increment the sent message count const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { - //TODO something qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); return {}; } @@ -2110,6 +2114,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out _event = encryptedEvent; +#endif } if (auto call = -- cgit v1.2.3 From 6f961ff2726c87e679cc9f6c39ed27a92a31cb0d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Apr 2022 23:32:59 +0200 Subject: Fixes --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 0ca8f648..35de59ed 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -452,7 +452,7 @@ public: qCWarning(E2EE) << "Failed to load key for new megolm session"; return; } - addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get(sessionKey), q->localUser()->id(), "SELF"_ls); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) -- cgit v1.2.3 From 89d8f6c44f86a27df28b1d89a80564fb0d4d89fc Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 16 May 2022 21:26:14 +0200 Subject: Fix build failures --- lib/room.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 35de59ed..d77bf9ef 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -448,11 +448,11 @@ public: connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); const auto sessionKey = currentOutboundMegolmSession->sessionKey(); - if(std::holds_alternative(sessionKey)) { + if(!sessionKey) { qCWarning(E2EE) << "Failed to load key for new megolm session"; return; } - addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get(sessionKey), q->localUser()->id(), "SELF"_ls); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -526,7 +526,7 @@ public: signedData.remove("unsigned"); signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); - if (std::holds_alternative(signatureMatch)) { + if (!signatureMatch) { qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; continue; } else { @@ -547,11 +547,11 @@ public: // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); - if(std::holds_alternative(_sessionKey)) { + if(!_sessionKey) { qCWarning(E2EE) << "Error loading session key"; return; } - const auto sessionKey = std::get(_sessionKey); + const auto sessionKey = *_sessionKey; const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; // Send the session to other people @@ -2101,11 +2101,11 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); - if(std::holds_alternative(encrypted)) { - qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); + if(!encrypted) { + qWarning(E2EE) << "Error encrypting message" << encrypted.error(); return {}; } - auto encryptedEvent = new EncryptedEvent(std::get(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + auto encryptedEvent = new EncryptedEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); -- cgit v1.2.3 From b29eb3954b798ac9110906cd79c4f288deaa2596 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 18 May 2022 22:39:27 +0200 Subject: Make database independent of {Room, User, Connection} --- lib/room.cpp | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index d77bf9ef..5d3ae329 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -455,16 +455,16 @@ public: addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } - std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) + std::unique_ptr payloadForUserDevice(QString user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) { // Noisy but nice for debugging //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); QJsonObject payloadJson = event->fullJson(); - payloadJson["recipient"] = user->id(); + payloadJson["recipient"] = user; payloadJson["sender"] = connection->user()->id(); QJsonObject recipientObject; - recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device); + recipientObject["ed25519"] = connection->edKeyForUserDevice(user, device); payloadJson["recipient_keys"] = recipientObject; QJsonObject senderObject; senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); @@ -472,22 +472,21 @@ public: payloadJson["sender_device"] = connection->deviceId(); auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); QJsonObject encrypted; - encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; + encrypted[connection->curveKeyForUserDevice(user, device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); } - QHash getDevicesWithoutKey() const + QHash getDevicesWithoutKey() const { - QHash devices; - auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId())); - for (const auto& user : rawDevices.keys()) { - devices[q->connection()->user(user)] = rawDevices[user]; + QHash devices; + for (const auto& user : q->users()) { + devices[user->id()] = q->connection()->devicesForUser(user->id()); } - return devices; + return q->connection()->database()->devicesWithoutKey(q->id(), devices, QString(currentOutboundMegolmSession->sessionId())); } - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) { qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; @@ -500,7 +499,7 @@ public: } } if (!u.isEmpty()) { - hash[user->id()] = u; + hash[user] = u; } } if (hash.isEmpty()) { @@ -512,38 +511,44 @@ public: const auto data = job->jsonData(); for(const auto &user : devices.keys()) { for(const auto &device : devices[user]) { - const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); + const auto recipientCurveKey = connection->curveKeyForUserDevice(user, device); if (!connection->hasOlmSession(user, device)) { qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) { + if(data["one_time_keys"][user][device].toObject().isEmpty()) { qWarning() << "No one time key for" << user << device; continue; } - const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0]; - const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString(); - const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject(); + const auto keyId = data["one_time_keys"][user][device].toObject().keys()[0]; + const auto oneTimeKey = data["one_time_keys"][user][device][keyId]["key"].toString(); + const auto signature = data["one_time_keys"][user][device][keyId]["signatures"][user][QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"][user][device][keyId].toObject(); signedData.remove("unsigned"); signedData.remove("signatures"); - auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); + auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user, device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (!signatureMatch) { - qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; + qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user << device << ". Skipping this device."; continue; } else { } connection->createOlmSession(recipientCurveKey, oneTimeKey); } - usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); + usersToDevicesToEvents[user][device] = payloadForUserDevice(user, device, sessionId, sessionKey); } } if (!usersToDevicesToEvents.empty()) { connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + QHash>> receivedDevices; + for (const auto& user : devices.keys()) { + for (const auto& device : devices[user]) { + receivedDevices[user] += {device, q->connection()->curveKeyForUserDevice(user, device) }; + } + } + connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index); } }); } - void sendMegolmSession(const QHash& devices) { + void sendMegolmSession(const QHash& devices) { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); -- cgit v1.2.3 From 41897df408c1398881bb8cf82ae0dc4503cefef7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 14:11:18 +0200 Subject: Use list of 3-tuple instead of map --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 5d3ae329..3696f808 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -537,10 +537,10 @@ public: } if (!usersToDevicesToEvents.empty()) { connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - QHash>> receivedDevices; + QVector> receivedDevices; for (const auto& user : devices.keys()) { for (const auto& device : devices[user]) { - receivedDevices[user] += {device, q->connection()->curveKeyForUserDevice(user, device) }; + receivedDevices += {user, device, q->connection()->curveKeyForUserDevice(user, device) }; } } connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index); -- cgit v1.2.3 From 146c2f73a22be32033a4999fd722cb92dcdf3c2f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 19 May 2022 14:15:40 +0200 Subject: Update lib/room.cpp Co-authored-by: Alexey Rusakov --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 3696f808..7a3c66b6 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2522,7 +2522,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); - Omittable encryptedFile = std::nullopt; + Omittable encryptedFile { none }; #ifdef Quotient_E2EE_ENABLED QTemporaryFile tempFile; if (usesEncryption()) { -- cgit v1.2.3 From 7b0de6473b6e23f1d74e7ad5739ad86c6b243797 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 14:32:38 +0200 Subject: Apply Suggestions --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7a3c66b6..e0494f83 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2551,7 +2551,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, file.url = QUrl(job->contentUri()); emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); } else { - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none); } }); connect(job, &BaseJob::failure, this, @@ -2620,7 +2620,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { d->fileTransfers[eventId].status = FileTransferInfo::Completed; emit fileTransferCompleted( - eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()), none); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, -- cgit v1.2.3 From 7a1283d2cc753781d4adbf4c69d3167651fce97b Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 14:54:00 +0200 Subject: Apply suggestions --- lib/room.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e0494f83..1f29d551 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2091,6 +2091,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. const RoomEvent* _event = pEvent; + std::unique_ptr encryptedEvent; if (q->usesEncryption()) { #ifndef Quotient_E2EE_ENABLED @@ -2110,7 +2111,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) qWarning(E2EE) << "Error encrypting message" << encrypted.error(); return {}; } - auto encryptedEvent = new EncryptedEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + encryptedEvent = makeEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); @@ -2118,7 +2119,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out - _event = encryptedEvent; + _event = encryptedEvent.get(); #endif } @@ -2151,9 +2152,6 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) << "already merged"; emit q->messageSent(txnId, call->eventId()); - if (q->usesEncryption()){ - delete _event; - } }); } else onEventSendingFailure(txnId); @@ -2553,6 +2551,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, } else { emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none); } + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); -- cgit v1.2.3 From 20f169af3df90198770e7017c399b08f7d313ebd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 19 May 2022 17:42:22 +0200 Subject: Fix FTBFS without E2EE --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1f29d551..7022a49d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,7 +12,6 @@ #include "avatar.h" #include "connection.h" #include "converters.h" -#include "e2ee/qolmoutboundsession.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -70,6 +69,7 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" +#include "e2ee/qolmoutboundsession.h" #include "e2ee/qolmutility.h" #include "database.h" #endif // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 0b5e72a2c6502f22a752b72b4df5fa25746fdd25 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 08:51:22 +0200 Subject: Refactor EncryptedFile and EC::FileInfo::file Besides having a misleading name (and it goes back to the spec), EncryptedFile under `file` key preempts the `url` (or `thumbnail_url`) string value so only one of the two should exist. This is a case for using std::variant<> - despite its clumsy syntax, it can actually simplify and streamline code when all the necessary bits are in place (such as conversion to JSON and getting the common piece - the URL - out of it). This commit replaces `FileInfo::url` and `FileInfo::file` with a common field `source` of type `FileSourceInfo` that is an alias for a variant type covering both underlying types; and `url()` is reintroduced as a function instead, to allow simplified access to whichever URL is available inside the variant. Oh, and EncryptedFile is EncryptedFileMetadata now, to clarify that it does not represent the file payload itself but rather the data necessary to obtain that payload. --- lib/room.cpp | 85 ++++++++++++++++++++++++++++-------------------------------- 1 file changed, 40 insertions(+), 45 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7022a49d..20ea1159 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1525,7 +1525,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) const auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); return connection()->getUrlForApi( - thumbnail->url, thumbnail->imageSize); + thumbnail->url(), thumbnail->imageSize); } qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; @@ -1536,7 +1536,7 @@ QUrl Room::urlToDownload(const QString& eventId) const if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); - return connection()->getUrlForApi(fileInfo->url); + return connection()->getUrlForApi(fileInfo->url()); } return {}; } @@ -2275,28 +2275,26 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) // Below, the upload job is used as a context object to clean up connections const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable encryptedFile) { - if (tId != txnId) - return; + [this, txnId](const QString& tId, const QUrl&, + const FileSourceInfo fileMetadata) { + if (tId != txnId) + return; - const auto it = q->findPendingEvent(txnId); - if (it != unsyncedEvents.end()) { - it->setFileUploaded(mxcUri); - if (encryptedFile) { - it->setEncryptedFile(*encryptedFile); - } - emit q->pendingEventChanged( - int(it - unsyncedEvents.begin())); - doSendEvent(it->get()); - } else { - // Normally in this situation we should instruct - // the media server to delete the file; alas, there's no - // API specced for that. - qCWarning(MAIN) << "File uploaded to" << mxcUri - << "but the event referring to it was " - "cancelled"; - } - }); + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(fileMetadata); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) + << "File uploaded to" << getUrlFromSourceInfo(fileMetadata) + << "but the event referring to it was " + "cancelled"; + } + }); connect(q, &Room::fileTransferFailed, transferJob, [this, txnId](const QString& tId) { if (tId != txnId) @@ -2322,13 +2320,13 @@ QString Room::postFile(const QString& plainText, Q_ASSERT(content != nullptr && content->fileInfo() != nullptr); const auto* const fileInfo = content->fileInfo(); Q_ASSERT(fileInfo != nullptr); - QFileInfo localFile { fileInfo->url.toLocalFile() }; + QFileInfo localFile { fileInfo->url().toLocalFile() }; Q_ASSERT(localFile.isFile()); return d->doPostFile( makeEvent( plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content), - fileInfo->url); + fileInfo->url()); } #if QT_VERSION_MAJOR < 6 @@ -2520,18 +2518,19 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); - Omittable encryptedFile { none }; + FileSourceInfo fileMetadata; #ifdef Quotient_E2EE_ENABLED QTemporaryFile tempFile; if (usesEncryption()) { tempFile.open(); QFile file(localFilename.toLocalFile()); file.open(QFile::ReadOnly); - auto [e, data] = EncryptedFile::encryptFile(file.readAll()); + QByteArray data; + std::tie(fileMetadata, data) = + EncryptedFileMetadata::encryptFile(file.readAll()); tempFile.write(data); tempFile.close(); fileName = QFileInfo(tempFile).absoluteFilePath(); - encryptedFile = e; } #endif auto job = connection()->uploadFile(fileName, overrideContentType); @@ -2542,17 +2541,13 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - if (encryptedFile) { - auto file = *encryptedFile; - file.url = QUrl(job->contentUri()); - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); - } else { - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none); - } - - }); + connect(job, &BaseJob::success, this, + [this, id, localFilename, job, fileMetadata]() mutable { + // The lambda is mutable to change encryptedFileMetadata + d->fileTransfers[id].status = FileTransferInfo::Completed; + setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri())); + emit fileTransferCompleted(id, localFilename, fileMetadata); + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); emit newFileTransfer(id, localFilename); @@ -2585,11 +2580,11 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) << "has an empty or malformed mxc URL; won't download"; return; } - const auto fileUrl = fileInfo->url; + const auto fileUrl = fileInfo->url(); auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { // Setup default file path filePath = - fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event); + fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event); if (filePath.size() > 200) // If too long, elide in the middle filePath.replace(128, filePath.size() - 192, "---"); @@ -2599,9 +2594,9 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) } DownloadFileJob *job = nullptr; #ifdef Quotient_E2EE_ENABLED - if(fileInfo->file.has_value()) { - auto file = *fileInfo->file; - job = connection()->downloadFile(fileUrl, file, filePath); + if (auto* fileMetadata = + std::get_if(&fileInfo->source)) { + job = connection()->downloadFile(fileUrl, *fileMetadata, filePath); } else { #endif job = connection()->downloadFile(fileUrl, filePath); @@ -2619,7 +2614,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { d->fileTransfers[eventId].status = FileTransferInfo::Completed; emit fileTransferCompleted( - eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()), none); + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, -- cgit v1.2.3 From 841846ea5efad80ce20e0d42b1885def224e58ad Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 11:03:16 +0200 Subject: Cleanup and fix Sonar warnings --- lib/room.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 20ea1159..0cef1025 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2130,7 +2130,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { - qCWarning(EVENTS) << "Pending event for transaction" << txnId + qWarning(EVENTS) << "Pending event for transaction" << txnId << "not found - got synced so soon?"; return; } @@ -2140,7 +2140,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId, _event] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -2148,7 +2148,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } } else - qCDebug(EVENTS) << "Pending event for transaction" << txnId + qDebug(EVENTS) << "Pending event for transaction" << txnId << "already merged"; emit q->messageSent(txnId, call->eventId()); @@ -2206,11 +2206,9 @@ QString Room::retryMessage(const QString& txnId) return d->doSendEvent(it->event()); } -// Lambda defers actual tr() invocation to the moment when translations are -// initialised -const auto FileTransferCancelledMsg = [] { - return Room::tr("File transfer cancelled"); -}; +// Using a function defers actual tr() invocation to the moment when +// translations are initialised +auto FileTransferCancelledMsg() { return Room::tr("File transfer cancelled"); } void Room::discardMessage(const QString& txnId) { -- cgit v1.2.3 From c2d87291dbf8bd240e3e96138ec52aa5da22416b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 12:50:30 +0200 Subject: Move encryptFile/decryptFile out of EncryptedFileMetadata These are not operations on EncryptedFileMetadata but rather on a combination of EncryptedFileMetadata and ciphertext. If C++ had multimethods these could be bound to such a combination. --- lib/room.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 0cef1025..4cb01a39 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2524,8 +2524,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, QFile file(localFilename.toLocalFile()); file.open(QFile::ReadOnly); QByteArray data; - std::tie(fileMetadata, data) = - EncryptedFileMetadata::encryptFile(file.readAll()); + std::tie(fileMetadata, data) = encryptFile(file.readAll()); tempFile.write(data); tempFile.close(); fileName = QFileInfo(tempFile).absoluteFilePath(); -- cgit v1.2.3 From c2e9256b1c334bdadcc208429084cbc83496fb4b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 12:57:23 +0200 Subject: Cleanup and address Sonar warnings --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4cb01a39..26fe80e3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2274,7 +2274,7 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, [this, txnId](const QString& tId, const QUrl&, - const FileSourceInfo fileMetadata) { + const FileSourceInfo& fileMetadata) { if (tId != txnId) return; -- cgit v1.2.3 From 64797165f04a16d290dd27c2f962060b40f85be3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 25 May 2022 22:48:53 +0200 Subject: Refactor creation of Megolm sessions in Room Notably, replace a multi-level hash map with QMultiHash and factor out Room::P::createOlmSession(). --- lib/room.cpp | 156 ++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 91 insertions(+), 65 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 26fe80e3..07d03467 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -443,9 +443,11 @@ public: return q->getCurrentState()->rotationPeriodMsgs(); } void createMegolmSession() { - qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); + qCDebug(E2EE) << "Creating new outbound megolm session for room " + << q->objectName(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); - connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); const auto sessionKey = currentOutboundMegolmSession->sessionKey(); if(!sessionKey) { @@ -477,90 +479,113 @@ public: return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); } - QHash getDevicesWithoutKey() const + QMultiHash getDevicesWithoutKey() const { - QHash devices; - for (const auto& user : q->users()) { - devices[user->id()] = q->connection()->devicesForUser(user->id()); + QMultiHash devices; + for (const auto& user : q->users()) + for (const auto& deviceId : connection->devicesForUser(user->id())) + devices.insert(user->id(), deviceId); + + return connection->database()->devicesWithoutKey( + id, devices, currentOutboundMegolmSession->sessionId()); + } + + bool createOlmSession(const QString& user, const QString& device, + const QJsonObject& oneTimeKeyObject) const + { + static QOlmUtility verifier; + qDebug(E2EE) << "Creating a new session for" << user << device; + if (oneTimeKeyObject.isEmpty()) { + qWarning(E2EE) << "No one time key for" << user << device; + return false; } - return q->connection()->database()->devicesWithoutKey(q->id(), devices, QString(currentOutboundMegolmSession->sessionId())); + const auto oneTimeKeyForId = *oneTimeKeyObject.constBegin(); + const auto signature = + oneTimeKeyForId["signatures"][user]["ed25519:"_ls % device] + .toString() + .toLatin1(); + auto signedObject = oneTimeKeyForId.toObject(); + signedObject.remove("unsigned"_ls); + signedObject.remove("signatures"_ls); + const auto signedData = + QJsonDocument(signedObject).toJson(QJsonDocument::Compact); + if (!verifier.ed25519Verify( + connection->edKeyForUserDevice(user, device).toLatin1(), + signedData, signature)) { + qWarning(E2EE) << "Failed to verify one-time-key signature for" + << user << device << ". Skipping this device."; + return false; + } + const auto recipientCurveKey = + connection->curveKeyForUserDevice(user, device); + connection->createOlmSession(recipientCurveKey, + oneTimeKeyForId["key"].toString()); + return true; } - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) + void sendRoomKeyToDevices(const QByteArray& sessionId, + const QByteArray& sessionKey, + const QMultiHash& devices, + int index) { - qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); + qDebug(E2EE) << "Sending room key to devices:" << sessionId + << sessionKey.toHex(); QHash> hash; - for (const auto& user : devices.keys()) { - QHash u; - for(const auto &device : devices[user]) { - if (!connection->hasOlmSession(user, device)) { - u[device] = "signed_curve25519"_ls; - qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; - } + for (const auto& [userId, deviceId] : asKeyValueRange(devices)) + if (!connection->hasOlmSession(userId, deviceId)) { + hash[userId].insert(deviceId, "signed_curve25519"_ls); + qDebug(E2EE) + << "Adding" << userId << deviceId << "to keys to claim"; } - if (!u.isEmpty()) { - hash[user] = u; - } - } - if (hash.isEmpty()) { + + if (hash.isEmpty()) return; - } + auto job = connection->callApi(hash); - connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ + connect(job, &BaseJob::success, q, + [job, this, sessionId, sessionKey, devices, index] { Connection::UsersToDevicesToEvents usersToDevicesToEvents; const auto data = job->jsonData(); - for(const auto &user : devices.keys()) { - for(const auto &device : devices[user]) { - const auto recipientCurveKey = connection->curveKeyForUserDevice(user, device); - if (!connection->hasOlmSession(user, device)) { - qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"][user][device].toObject().isEmpty()) { - qWarning() << "No one time key for" << user << device; - continue; - } - const auto keyId = data["one_time_keys"][user][device].toObject().keys()[0]; - const auto oneTimeKey = data["one_time_keys"][user][device][keyId]["key"].toString(); - const auto signature = data["one_time_keys"][user][device][keyId]["signatures"][user][QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"][user][device][keyId].toObject(); - signedData.remove("unsigned"); - signedData.remove("signatures"); - auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user, device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); - if (!signatureMatch) { - qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user << device << ". Skipping this device."; - continue; - } else { - } - connection->createOlmSession(recipientCurveKey, oneTimeKey); - } - usersToDevicesToEvents[user][device] = payloadForUserDevice(user, device, sessionId, sessionKey); - } + for (const auto& [user, device] : asKeyValueRange(devices)) { + if (!connection->hasOlmSession(user, device) + && !createOlmSession( + user, device, + data["one_time_keys"][user][device].toObject())) + continue; + + usersToDevicesToEvents[user][device] = + payloadForUserDevice(user, device, sessionId, + sessionKey); } if (!usersToDevicesToEvents.empty()) { - connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->sendToDevices("m.room.encrypted"_ls, + usersToDevicesToEvents); QVector> receivedDevices; - for (const auto& user : devices.keys()) { - for (const auto& device : devices[user]) { - receivedDevices += {user, device, q->connection()->curveKeyForUserDevice(user, device) }; - } - } - connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index); + receivedDevices.reserve(devices.size()); + for (const auto& [user, device] : asKeyValueRange(devices)) + receivedDevices.push_back( + { user, device, + connection->curveKeyForUserDevice(user, device) }); + + connection->database()->setDevicesReceivedKey(id, + receivedDevices, + sessionId, index); } }); } - void sendMegolmSession(const QHash& devices) { + void sendMegolmSession(const QMultiHash& devices) { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); - const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); - if(!_sessionKey) { + const auto sessionKey = currentOutboundMegolmSession->sessionKey(); + if(!sessionKey) { qCWarning(E2EE) << "Error loading session key"; return; } - const auto sessionKey = *_sessionKey; - const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; // Send the session to other people - sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex()); + sendRoomKeyToDevices(sessionId, *sessionKey, devices, + currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED @@ -592,7 +617,8 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) } }); d->groupSessions = connection->loadRoomMegolmSessions(this); - d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this); + d->currentOutboundMegolmSession = + connection->loadCurrentOutboundMegolmSession(this->id()); if (d->shouldRotateMegolmSession()) { d->currentOutboundMegolmSession = nullptr; } @@ -2101,12 +2127,12 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); } - const auto devicesWithoutKey = getDevicesWithoutKey(); - sendMegolmSession(devicesWithoutKey); + sendMegolmSession(getDevicesWithoutKey()); const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); - connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); if(!encrypted) { qWarning(E2EE) << "Error encrypting message" << encrypted.error(); return {}; -- cgit v1.2.3 From 0f8335a32debc4c61d9fc9875c79c0ba6ba05357 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 27 May 2022 19:09:26 +0200 Subject: Move some Meg/Olm session logic from Room::Private to Connection::Private Functions (Room::Private::)createOlmSession, payloadForUserDevice and sendRoomKeyToDevices don't have a lot to do with the given Room object but deal with quite a few things stored in Connection. This commit moves them to Connection::Private, exposing sendSessionKeyToDevices (the new name for sendRoomKeyToDevices) in Connection so that Room could call it from Room::P::sendMegolmSession(). While moving these over, a few additional things were adjusted: - more functions marked as const - a few functions could be moved now from Connection to Connection::Private - false slots in Connection (such as picklingMode) are moved out of the slots block - keys.yml in Matrix CS API definitions has been adjusted to match the real structure of `/claim` response (see quotient-im/matrix-spec repo); csapi/keys.h has been regenerated accordingly. --- lib/room.cpp | 113 +++-------------------------------------------------------- 1 file changed, 4 insertions(+), 109 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 07d03467..6ec06aa8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -457,28 +457,6 @@ public: addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } - std::unique_ptr payloadForUserDevice(QString user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) - { - // Noisy but nice for debugging - //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); - const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); - QJsonObject payloadJson = event->fullJson(); - payloadJson["recipient"] = user; - payloadJson["sender"] = connection->user()->id(); - QJsonObject recipientObject; - recipientObject["ed25519"] = connection->edKeyForUserDevice(user, device); - payloadJson["recipient_keys"] = recipientObject; - QJsonObject senderObject; - senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); - payloadJson["keys"] = senderObject; - payloadJson["sender_device"] = connection->deviceId(); - auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); - QJsonObject encrypted; - encrypted[connection->curveKeyForUserDevice(user, device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; - - return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); - } - QMultiHash getDevicesWithoutKey() const { QMultiHash devices; @@ -490,91 +468,7 @@ public: id, devices, currentOutboundMegolmSession->sessionId()); } - bool createOlmSession(const QString& user, const QString& device, - const QJsonObject& oneTimeKeyObject) const - { - static QOlmUtility verifier; - qDebug(E2EE) << "Creating a new session for" << user << device; - if (oneTimeKeyObject.isEmpty()) { - qWarning(E2EE) << "No one time key for" << user << device; - return false; - } - const auto oneTimeKeyForId = *oneTimeKeyObject.constBegin(); - const auto signature = - oneTimeKeyForId["signatures"][user]["ed25519:"_ls % device] - .toString() - .toLatin1(); - auto signedObject = oneTimeKeyForId.toObject(); - signedObject.remove("unsigned"_ls); - signedObject.remove("signatures"_ls); - const auto signedData = - QJsonDocument(signedObject).toJson(QJsonDocument::Compact); - if (!verifier.ed25519Verify( - connection->edKeyForUserDevice(user, device).toLatin1(), - signedData, signature)) { - qWarning(E2EE) << "Failed to verify one-time-key signature for" - << user << device << ". Skipping this device."; - return false; - } - const auto recipientCurveKey = - connection->curveKeyForUserDevice(user, device); - connection->createOlmSession(recipientCurveKey, - oneTimeKeyForId["key"].toString()); - return true; - } - - void sendRoomKeyToDevices(const QByteArray& sessionId, - const QByteArray& sessionKey, - const QMultiHash& devices, - int index) - { - qDebug(E2EE) << "Sending room key to devices:" << sessionId - << sessionKey.toHex(); - QHash> hash; - for (const auto& [userId, deviceId] : asKeyValueRange(devices)) - if (!connection->hasOlmSession(userId, deviceId)) { - hash[userId].insert(deviceId, "signed_curve25519"_ls); - qDebug(E2EE) - << "Adding" << userId << deviceId << "to keys to claim"; - } - - if (hash.isEmpty()) - return; - - auto job = connection->callApi(hash); - connect(job, &BaseJob::success, q, - [job, this, sessionId, sessionKey, devices, index] { - Connection::UsersToDevicesToEvents usersToDevicesToEvents; - const auto data = job->jsonData(); - for (const auto& [user, device] : asKeyValueRange(devices)) { - if (!connection->hasOlmSession(user, device) - && !createOlmSession( - user, device, - data["one_time_keys"][user][device].toObject())) - continue; - - usersToDevicesToEvents[user][device] = - payloadForUserDevice(user, device, sessionId, - sessionKey); - } - if (!usersToDevicesToEvents.empty()) { - connection->sendToDevices("m.room.encrypted"_ls, - usersToDevicesToEvents); - QVector> receivedDevices; - receivedDevices.reserve(devices.size()); - for (const auto& [user, device] : asKeyValueRange(devices)) - receivedDevices.push_back( - { user, device, - connection->curveKeyForUserDevice(user, device) }); - - connection->database()->setDevicesReceivedKey(id, - receivedDevices, - sessionId, index); - } - }); - } - - void sendMegolmSession(const QMultiHash& devices) { + void sendMegolmSession(const QMultiHash& devices) const { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto sessionKey = currentOutboundMegolmSession->sessionKey(); @@ -584,8 +478,9 @@ public: } // Send the session to other people - sendRoomKeyToDevices(sessionId, *sessionKey, devices, - currentOutboundMegolmSession->sessionMessageIndex()); + connection->sendSessionKeyToDevices( + id, sessionId, *sessionKey, devices, + currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 945b1ab33c92b7b13e2c6b6ebc1f56c5caee0f79 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 31 May 2022 19:07:01 +0200 Subject: Fix Room::getPreviousContent() to match new CS API There was a fairly nasty change where `from` parameter in /messages became optional and that led to two QString parameters (`from` and `dir) switching positions. Because they have the same type, the problem only shows at runtime. This commit fixes Room::getPreviousContent() to pass the parameters at right positions; client code won't feel anything (unless it uses GetRoomEventsJob directly). --- lib/room.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7022a49d..f85591c7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2464,15 +2464,18 @@ void Room::hangupCall(const QString& callId) d->sendEvent(callId); } -void Room::getPreviousContent(int limit, const QString &filter) { d->getPreviousContent(limit, filter); } +void Room::getPreviousContent(int limit, const QString& filter) +{ + d->getPreviousContent(limit, filter); +} void Room::Private::getPreviousContent(int limit, const QString &filter) { if (isJobPending(eventsHistoryJob)) return; - eventsHistoryJob = - connection->callApi(id, prevBatch, "b", "", limit, filter); + eventsHistoryJob = connection->callApi(id, "b", prevBatch, + "", limit, filter); emit q->eventsHistoryJobChanged(); connect(eventsHistoryJob, &BaseJob::success, q, [this] { prevBatch = eventsHistoryJob->end(); -- cgit v1.2.3 From 46ed9cc3336231d418449046ff5486b7abc9c925 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 1 Jun 2022 23:32:16 +0200 Subject: Immediately create a new megolm session when user leaves instead of deferring until sending event --- lib/room.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c745975f..9e2a5053 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -521,7 +521,9 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) if (!usesEncryption()) { return; } - d->currentOutboundMegolmSession = nullptr; + if (d->hasValidMegolmSession()) { + d->createMegolmSession(); + } qCDebug(E2EE) << "Invalidating current megolm session because user left"; }); -- cgit v1.2.3 From 2ecc08357fab7d22947b9cb5251d2f29be2ec55b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 4 Jun 2022 22:35:06 +0200 Subject: Address Sonar warnings --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 9e2a5053..284d19df 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1531,7 +1531,7 @@ QStringList Room::safeMemberNames() const { QStringList res; res.reserve(d->membersMap.size()); - for (auto u: std::as_const(d->membersMap)) + for (const auto* u: std::as_const(d->membersMap)) res.append(safeMemberName(u->id())); return res; @@ -1541,7 +1541,7 @@ QStringList Room::htmlSafeMemberNames() const { QStringList res; res.reserve(d->membersMap.size()); - for (auto u: std::as_const(d->membersMap)) + for (const auto* u: std::as_const(d->membersMap)) res.append(htmlSafeMemberName(u->id())); return res; @@ -3378,7 +3378,7 @@ QString Room::Private::calculateDisplayname() const shortlist = buildShortlist(membersLeft); QStringList names; - for (auto u : shortlist) { + for (const auto* u : shortlist) { if (u == nullptr || isLocalUser(u)) break; // Only disambiguate if the room is not empty -- cgit v1.2.3 From 2504e6e5f216e34fc9aabfda0c462b1b37620a5e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 17 Jun 2022 10:40:49 +0200 Subject: Further fix building with Qt 6 Also: build with Qt 6 first, so that it fails sooner. --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 284d19df..f692c354 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -118,7 +118,7 @@ public: // A map from evtId to a map of relation type to a vector of event // pointers. Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. - QHash, RelatedEvents> relations; + QHash, RelatedEvents> relations; QString displayname; Avatar avatar; QHash notifications; @@ -2687,7 +2687,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } if (const auto* reaction = eventCast(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; - const QPair lookupKey { targetEvtId, EventRelation::AnnotationType }; + const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType }; if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); emit q->updatedEvent(targetEvtId); -- cgit v1.2.3 From 6b355d1aa87072143e09ea5269e8cf465318a64f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 21:26:43 +0200 Subject: Drop pre-Qt 5.15 code --- lib/room.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f692c354..e07de123 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1974,11 +1974,8 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) if (changes & Change::Highlights) emit q->highlightCountChanged(); - qCDebug(MAIN) << terse << changes << "= hex" << -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - Qt:: -#endif - hex << uint(changes) << "in" << q->objectName(); + qCDebug(MAIN) << terse << changes << "= hex" << Qt::hex << uint(changes) + << "in" << q->objectName(); emit q->changed(changes); if (saveState) connection->saveRoomState(q); -- cgit v1.2.3 From 23672cbb025814f2d8d6960f72ab602ef7a55b41 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 17 Jun 2022 22:59:45 +0200 Subject: room.cpp: replace two signal connections with one --- lib/room.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index e07de123..2d4cfeb4 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2057,10 +2057,11 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) it->setDeparted(); emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); - Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, this, - txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId] { + Room::connect(call, &BaseJob::result, q, [this, txnId, call] { + if (!call->status().good()) { + onEventSendingFailure(txnId, call); + return; + } auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { -- cgit v1.2.3 From 143ac38aabab90b40fbe73b489b91fb159e66b6b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 23 Jun 2022 08:34:53 +0200 Subject: Streamline Room::P::shouldRotateMegolmSession() Now there's only 1 instead of 5 lookups of the same EncryptionEvent, and the code is shorter. --- lib/room.cpp | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 2d4cfeb4..35a89cc6 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -411,10 +411,17 @@ public: bool shouldRotateMegolmSession() const { - if (!q->usesEncryption()) { + const auto* encryptionConfig = currentState.get(); + if (!encryptionConfig || !encryptionConfig->useEncryption()) return false; - } - return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime(); + + const auto rotationInterval = encryptionConfig->rotationPeriodMs(); + const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs(); + return currentOutboundMegolmSession->messageCount() + >= rotationMessageCount + || currentOutboundMegolmSession->creationTime().addMSecs( + rotationInterval) + < QDateTime::currentDateTime(); } bool hasValidMegolmSession() const @@ -425,23 +432,6 @@ public: return currentOutboundMegolmSession != nullptr; } - /// Time in milliseconds after which the outgoing megolmsession should be replaced - unsigned int rotationInterval() const - { - if (!q->usesEncryption()) { - return 0; - } - return q->getCurrentState()->rotationPeriodMs(); - } - - // Number of messages sent by this user after which the outgoing megolm session should be replaced - int rotationMessageCount() const - { - if (!q->usesEncryption()) { - return 0; - } - return q->getCurrentState()->rotationPeriodMsgs(); - } void createMegolmSession() { qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->objectName(); -- cgit v1.2.3 From 0d52a733176fb3cb21daad12cf0a44178553d48b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 9 May 2022 12:03:09 +0200 Subject: Reuse Room::setState() overloads from one another --- lib/room.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 35a89cc6..2625105c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2263,8 +2263,7 @@ QString Room::postJson(const QString& matrixType, SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) { - return d->requestSetState(evt.matrixType(), evt.stateKey(), - evt.contentJson()); + return setState(evt.matrixType(), evt.stateKey(), evt.contentJson()); } SetRoomStateWithKeyJob* Room::setState(const QString& evtType, -- cgit v1.2.3 From 44aeb57e196bcd9e041c498a212f17b0dcd1244f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 02:52:35 +0200 Subject: Refactor things around EncryptionEvent[Content] EncryptionEvent was marked as Q_GADGET only for the sake of defining EncryptionType inside of it as Q_ENUM, with aliases also available under Quotient:: and EncryptionEventContent. This is a legacy from pre-Q_ENUM_NS times. However, event types are not really made to be proper Q_GADGETs: Q_GADGET implies access by value or reference but event types are uncopyable for the former and QML is ill-equipped for the latter. This commit moves EncryptionType definition to where other such enumerations reside - on the namespace level in quotient_common.h; and the other two places are now deprecated; and EncryptionEvent is no more Q_GADGET. With fromJson/toJson refactored in the previous commit there's no more need to specialise JsonConverter<>: specialising fromJson() is just enough. Moving EncryptionType to quotient_common.h exposed the clash of two Undefined enumerators (in RoomType and EncryptionType), warranting both enumerations to become scoped (which they ought to be, anyway). And while we're at it, the base type of enumerations is specified explicitly, as MSVC apparently uses a signed base type (int?) by default, unlike other compilers, and the upcoming enum converters will assume an unsigned base type. Finally, using fillFromJson() instead of fromJson() in the EncryptionEventContent constructor allowed to make default values explicit in the header file, rather than buried in the initialisation code. --- lib/room.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 2625105c..f67451ce 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -3062,7 +3062,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return false; } if (oldEncEvt - && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { + && oldEncEvt->encryption() != EncryptionType::Undefined) { qCWarning(STATE) << "The room is already encrypted but a new" " room encryption event arrived - ignoring"; return false; @@ -3508,5 +3508,5 @@ void Room::activateEncryption() qCWarning(E2EE) << "Room" << objectName() << "is already encrypted"; return; } - setState(EncryptionEventContent::MegolmV1AesSha2); + setState(EncryptionType::MegolmV1AesSha2); } -- cgit v1.2.3 From cc8753612f2f45b18a9c924920395fb5f4c57078 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 16 Jul 2022 21:54:49 +0200 Subject: Room::decryptIncomingEvents() The result of factoring out duplicate code. --- lib/room.cpp | 61 ++++++++++++++++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 33 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f67451ce..ba4b1d27 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -277,6 +277,7 @@ public: * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; + void decryptIncomingEvents(RoomEvents& events); Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker, ReadReceipt newReceipt = {}, @@ -1612,8 +1613,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, continue; auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())]; if (auto encryptedEvent = ti.viewAs()) { - auto decrypted = decryptMessage(*encryptedEvent); - if(decrypted) { + if (auto decrypted = decryptMessage(*encryptedEvent)) { // The reference will survive the pointer being moved auto& decryptedEvent = *decrypted; auto oldEvent = ti.replaceEvent(std::move(decrypted)); @@ -2572,6 +2572,26 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const events.erase(dupsBegin, events.end()); } +void Room::Private::decryptIncomingEvents(RoomEvents& events) +{ +#ifdef Quotient_E2EE_ENABLED + QElapsedTimer et; + et.start(); + size_t totalDecrypted = 0; + for (auto& eptr : events) + if (const auto& eeptr = eventCast(eptr)) { + if (auto decrypted = q->decryptMessage(*eeptr)) { + ++totalDecrypted; + auto&& oldEvent = exchange(eptr, move(decrypted)); + eptr->setOriginalEvent(::move(oldEvent)); + } else + undecryptedEvents[eeptr->sessionId()] += eeptr->id(); + } + if (totalDecrypted > 5 || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) << "Decrypted" << totalDecrypted << "events in" << et; +#endif +} + /** Make a redacted event * * This applies the redaction procedure as defined by the CS API specification @@ -2762,23 +2782,11 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (events.empty()) return Change::None; + decryptIncomingEvents(events); + QElapsedTimer et; et.start(); -#ifdef Quotient_E2EE_ENABLED - for(long unsigned int i = 0; i < events.size(); i++) { - if(auto* encrypted = eventCast(events[i])) { - auto decrypted = q->decryptMessage(*encrypted); - if(decrypted) { - auto oldEvent = std::exchange(events[i], std::move(decrypted)); - events[i]->setOriginalEvent(std::move(oldEvent)); - } else { - undecryptedEvents[encrypted->sessionId()] += encrypted->id(); - } - } - } -#endif - { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -2922,30 +2930,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { - QElapsedTimer et; - et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); if (events.empty()) return; - Changes changes {}; - -#ifdef Quotient_E2EE_ENABLED - for(long unsigned int i = 0; i < events.size(); i++) { - if(auto* encrypted = eventCast(events[i])) { - auto decrypted = q->decryptMessage(*encrypted); - if(decrypted) { - auto oldEvent = std::exchange(events[i], std::move(decrypted)); - events[i]->setOriginalEvent(std::move(oldEvent)); - } else { - undecryptedEvents[encrypted->sessionId()] += encrypted->id(); - } - } - } -#endif + decryptIncomingEvents(events); + QElapsedTimer et; + et.start(); + Changes changes {}; // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly -- cgit v1.2.3 From 4770d303b7141971fa9a25f85874e6bbe71776d9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 16 Jul 2022 20:31:12 +0200 Subject: Speed up read receipt updates Profiling revealed 3 inefficiencies in read receipts code - and given there are a lot of them coming, these inefficiences quickly add up. Fixing them allows to slash read receipt processing time by 60%, and the total time of updating a room by more than a half. 1. Room::lastReadEventChanged() is emitted per receipt. This can be taxing on initial syncs or in bigger rooms; this commit converts it to an aggregate signal only emitted once per sync room batch and carrying the list of all user ids (more on that below) with updated read receipts. For that, Room::P::setLastReadEvent() is split into Room::P::setLocalLastReadEvent() that is called whenever the local read receipt has to be updated, and setLastReadEvent() proper that is very fast and only updates the internal data structures, nothing else. setLocalLastEvent() calls it, as does processEphemeralEvents(); both take responsibility to emit lastReadEventChanged() depending on the outcome of setLastReadEvent() invocation(s). 2. Massively aggravating the above point, user id from each read receipt is turned to a User object - and since most of the users are unknown at early moments, this causes thousands of allocations. Therefore the new aggregated lastReadEventChanged() only carries user ids, and clients will have to resolve them to User objects if they need. 3. Despite fairly tight conditions (note we're talking about thousands of receipts), Quotient still creates an intermediate C++ structure (EventsWithReceipts), only for the sake of passing it to processEphemeralEvent() that immediately disassembles it back again, converting to a series of calls to set(Local)LastReadEvent(). To fix this, processEphemeralEvent() now takes the event content JSON directly and iterates over it instead. Aside from that, a few extraneous conditions and logging has been removed and the whole function rewritten with switchOnType() to reduce cognitive complexity. --- lib/room.cpp | 190 ++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 109 insertions(+), 81 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ba4b1d27..b128d2a7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -279,9 +279,15 @@ public: void dropDuplicateEvents(RoomEvents& events) const; void decryptIncomingEvents(RoomEvents& events); - Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker, - ReadReceipt newReceipt = {}, - bool deferStatsUpdate = false); + //! \brief update last receipt record for a given user + //! + //! \return previous event id of the receipt if the new receipt changed + //! it, or `none` if no change took place + Omittable setLastReadReceipt(const QString& userId, rev_iter_t newMarker, + ReadReceipt newReceipt = {}); + Changes setLocalLastReadReceipt(const rev_iter_t& newMarker, + ReadReceipt newReceipt = {}, + bool deferStatsUpdate = false); Changes setFullyReadMarker(const QString &eventId); Changes updateStats(const rev_iter_t& from, const rev_iter_t& to); bool markMessagesAsRead(const rev_iter_t& upToMarker); @@ -701,10 +707,9 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -Room::Changes Room::Private::setLastReadReceipt(const QString& userId, - rev_iter_t newMarker, - ReadReceipt newReceipt, - bool deferStatsUpdate) +Omittable Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, + ReadReceipt newReceipt) { if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) newMarker = q->findInTimeline(newReceipt.eventId); @@ -718,7 +723,7 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, // eagerMarker is now just after the desired event for newMarker if (eagerMarker != newMarker.base()) { newMarker = rev_iter_t(eagerMarker); - qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId + qDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId << "to" << *newMarker; } // Fill newReceipt with the event (and, if needed, timestamp) from @@ -732,14 +737,19 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, const auto prevEventId = storedReceipt.eventId; // Check that either the new marker is actually "newer" than the current one // or, if both markers are at historyEdge(), event ids are different. + // This logic tackles, in particular, the case when the new event is not + // found (most likely, because it's too old and hasn't been fetched from + // the server yet) but there is a previous marker for a user; in that case, + // the previous marker is kept because read receipts are not supposed + // to move backwards. If neither new nor old event is found, the new receipt + // is blindly stored, in a hope it's also "newer" in the timeline. // NB: with reverse iterators, timeline history edge >= sync edge if (prevEventId == newReceipt.eventId || newMarker > q->findInTimeline(prevEventId)) - return Change::None; + return {}; // Finally make the change - Changes changes = Change::Other; auto oldEventReadUsersIt = eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member if (oldEventReadUsersIt != eventIdReadUsers.end()) { @@ -751,7 +761,7 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, storedReceipt = move(newReceipt); { - auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug + auto dbg = qDebug(EPHEMERAL); // NB: qCDebug can't be used like that dbg << "The new read receipt for" << userId << "is now at"; if (newMarker == historyEdge()) dbg << storedReceipt.eventId; @@ -759,25 +769,37 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, dbg << *newMarker; } - // TODO: use Room::member() when it becomes a thing and only emit signals - // for actual members, not just any user - const auto member = q->user(userId); - Q_ASSERT(member != nullptr); - if (isLocalUser(member) && !deferStatsUpdate) { - if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId), + // NB: This method, unlike setLocalLastReadReceipt, doesn't emit + // lastReadEventChanged() to avoid numerous emissions when many read + // receipts arrive. It can be called thousands of times during an initial + // sync, e.g. + // TODO: remove in 0.8 + if (const auto member = q->user(userId); !isLocalUser(member)) + emit q->readMarkerForUserMoved(member, prevEventId, + storedReceipt.eventId); + return prevEventId; +} + +Room::Changes Room::Private::setLocalLastReadReceipt(const rev_iter_t& newMarker, + ReadReceipt newReceipt, + bool deferStatsUpdate) +{ + auto prevEventId = + setLastReadReceipt(connection->userId(), newMarker, move(newReceipt)); + if (!prevEventId) + return Change::None; + Changes changes = Change::Other; + if (!deferStatsUpdate) { + if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(*prevEventId), newMarker)) { - qCDebug(MESSAGES) + qDebug(MESSAGES) << "Updated unread event statistics in" << q->objectName() << "after moving the local read receipt:" << unreadStats; changes |= Change::UnreadStats; } Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check } - emit q->lastReadEventChanged(member); - // TODO: remove in 0.8 - if (!isLocalUser(member)) - emit q->readMarkerForUserMoved(member, prevEventId, - storedReceipt.eventId); + emit q->lastReadEventChanged({ connection->userId() }); return changes; } @@ -792,7 +814,7 @@ Room::Changes Room::Private::updateStats(const rev_iter_t& from, Changes changes = Change::None; // Correct the read receipt to never be behind the fully read marker if (readReceiptMarker > fullyReadMarker - && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) { + && setLocalLastReadReceipt(fullyReadMarker, {}, true)) { changes |= Change::Other; readReceiptMarker = q->localReadReceiptMarker(); qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read " @@ -890,7 +912,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;) if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { // Pull read receipt if it's behind, and update statistics - changes |= setLastReadReceipt(connection->userId(), rm); + changes |= setLocalLastReadReceipt(rm); if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) { changes |= Change::PartiallyReadStats; qCDebug(MESSAGES) @@ -908,9 +930,8 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) void Room::setReadReceipt(const QString& atEventId) { - if (const auto changes = d->setLastReadReceipt(localUser()->id(), - historyEdge(), - { atEventId })) { + if (const auto changes = + d->setLocalLastReadReceipt(historyEdge(), { atEventId })) { connection()->callApi(BackgroundRequest, id(), QStringLiteral("m.read"), QUrl::toPercentEncoding(atEventId)); @@ -3198,59 +3219,66 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) Changes changes {}; QElapsedTimer et; et.start(); - if (auto* evt = eventCast(event)) { - const auto& users = evt->users(); - d->usersTyping.clear(); - d->usersTyping.reserve(users.size()); // Assume all are members - for (const auto& userId : users) - if (isMember(userId)) - d->usersTyping.append(user(userId)); - - if (users.size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) - << "Processing typing events from" << users.size() - << "user(s) in" << objectName() << "took" << et; - emit typingChanged(); - } - if (auto* evt = eventCast(event)) { - int totalReceipts = 0; - const auto& eventsWithReceipts = evt->eventsWithReceipts(); - for (const auto& p : eventsWithReceipts) { - totalReceipts += p.receipts.size(); - const auto newMarker = findInTimeline(p.evtId); - if (newMarker == historyEdge()) - qCDebug(EPHEMERAL) - << "Event" << p.evtId - << "is not found; saving read receipt(s) anyway"; - // If the event is not found (most likely, because it's too old and - // hasn't been fetched from the server yet) but there is a previous - // marker for a user, keep the previous marker because read receipts - // are not supposed to move backwards. Otherwise, blindly store - // the event id for this user and update the read marker when/if - // the event is fetched later on. - const auto updatedCount = std::count_if( - p.receipts.cbegin(), p.receipts.cend(), - [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) { - const auto change = - d->setLastReadReceipt(r.userId, newMarker, - { evtId, r.timestamp }); - changes |= change; - return change & Change::Any; - }); - - if (p.receipts.size() > 1) - qCDebug(EPHEMERAL) << p.evtId << "marked as read for" - << updatedCount << "user(s)"; - if (updatedCount < p.receipts.size()) - qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount - << "receipts were skipped"; - } - if (eventsWithReceipts.size() > 3 || totalReceipts > 10 - || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "Processing" << totalReceipts - << "receipt(s) on" << eventsWithReceipts.size() - << "event(s) in" << objectName() << "took" << et; - } + switchOnType(*event, + [this, &et](const TypingEvent& evt) { + const auto& users = evt.users(); + d->usersTyping.clear(); + d->usersTyping.reserve(users.size()); // Assume all are members + for (const auto& userId : users) + if (isMember(userId)) + d->usersTyping.append(user(userId)); + + if (d->usersTyping.size() > 3 + || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) + << "Processing typing events from" << users.size() + << "user(s) in" << objectName() << "took" << et; + emit typingChanged(); + }, + [this, &changes, &et](const ReceiptEvent& evt) { + const auto& receiptsJson = evt.contentJson(); + QVector updatedUserIds; + // Most often (especially for bigger batches), receipts are + // scattered across events (an anecdotal evidence showed 1.2-1.3 + // receipts per event on average). + updatedUserIds.reserve(receiptsJson.size() * 2); + for (auto eventIt = receiptsJson.begin(); + eventIt != receiptsJson.end(); ++eventIt) { + const auto evtId = eventIt.key(); + const auto newMarker = findInTimeline(evtId); + if (newMarker == historyEdge()) + qDebug(EPHEMERAL) + << "Event" << evtId + << "is not found; saving read receipt(s) anyway"; + const auto reads = + eventIt.value().toObject().value("m.read"_ls).toObject(); + for (auto userIt = reads.begin(); userIt != reads.end(); + ++userIt) { + ReadReceipt rr{ evtId, + fromJson( + userIt->toObject().value("ts"_ls)) }; + const auto userId = userIt.key(); + if (userId == connection()->userId()) { + // Local user is special, and will get a signal about + // its read receipt separately from (and before) a + // signal on everybody else. No particular reason, just + // less cumbersome code. + changes |= d->setLocalLastReadReceipt(newMarker, rr); + } else if (d->setLastReadReceipt(userId, newMarker, rr)) { + changes |= Change::Other; + updatedUserIds.push_back(userId); + } + } + } + if (updatedUserIds.size() > 10 + || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) + << "Processing" << updatedUserIds + << "non-local receipt(s) on" << receiptsJson.size() + << "event(s) in" << objectName() << "took" << et; + if (!updatedUserIds.empty()) + emit lastReadEventChanged(updatedUserIds); + }); return changes; } -- cgit v1.2.3 From aac349a2b3fe643b55808586f461642ea15da8e5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jul 2022 13:12:52 +0200 Subject: Don't redact certain event types even though lib doesn't know them Event type ids don't need a C++ type to be used, and clients might define those types on their side (NeoChat does that, e.g.). --- lib/room.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index b128d2a7..53474c24 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2640,10 +2640,11 @@ RoomEventPtr makeRedacted(const RoomEvent& target, { QStringLiteral("ban"), QStringLiteral("events"), QStringLiteral("events_default"), QStringLiteral("kick"), QStringLiteral("redact"), QStringLiteral("state_default"), - QStringLiteral("users"), QStringLiteral("users_default") } } - // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } - // , { RoomHistoryVisibility::typeId(), - // { QStringLiteral("history_visibility") } } + QStringLiteral("users"), QStringLiteral("users_default") } }, + // TODO: Replace with RoomJoinRules::TypeId etc. once available + { "m.room.join_rules"_ls, { QStringLiteral("join_rule") } }, + { "m.room.history_visibility"_ls, + { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { if (!keepKeys.contains(it.key())) -- cgit v1.2.3 From c0ecba539aecebc7f3e32f40fc1d45d80d5e1f5c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jul 2022 16:07:47 +0200 Subject: Fix accidentally logging all receipt authors ...instead of just the number of them. --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 53474c24..f5d8709b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -3274,7 +3274,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) if (updatedUserIds.size() > 10 || et.nsecsElapsed() >= profilerMinNsecs()) qDebug(PROFILER) - << "Processing" << updatedUserIds + << "Processing" << updatedUserIds.size() << "non-local receipt(s) on" << receiptsJson.size() << "event(s) in" << objectName() << "took" << et; if (!updatedUserIds.empty()) -- cgit v1.2.3 From 3214feeb031fa231c7c42c21c53410302966e32e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 29 Jul 2022 18:13:14 +0200 Subject: eventloader.h: use basicJson() in a uniform way There's no particular reason the order of parameters in StateEventBase::basicJson() should be as it was, and (the only) loadStateEvent() usage in room.cpp suggests the unified order is more convenient. Besides, this order is aligned with that in the StateEventBase constructor. --- lib/room.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f5d8709b..7fd41a4f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -219,8 +219,9 @@ public: // In the absence of a real event, make a stub as-if an event // with empty content has been received. Event classes should be // prepared for empty/invalid/malicious content anyway. - stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {}, - evtKey.second)); + stubbedState.emplace(evtKey, + loadEvent(evtKey.first, + evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; qCDebug(STATE) << "Stubbed state size:" << stubbedState.size(); -- cgit v1.2.3 From 740e7a8c9fb4140c5eccd35bf4c78925754736db Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 10 Aug 2022 22:28:56 +0200 Subject: Emit Room::newFileTransfer when downloading a file --- lib/room.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 7fd41a4f..c6fbb353 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2551,6 +2551,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, job->errorString())); + emit newFileTransfer(eventId, localFilename); } else d->failedTransfer(eventId); } -- cgit v1.2.3 From 4ad2f6e165a4eb486155eae652e187dc4d6b7d99 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 20:02:35 +0200 Subject: Cleanup --- lib/room.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index c6fbb353..007446dc 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -16,6 +16,7 @@ #include "user.h" #include "eventstats.h" #include "roomstateview.h" +#include "qt_connection_util.h" // NB: since Qt 6, moc_room.cpp needs User fully defined #include "moc_room.cpp" -- cgit v1.2.3 From a26147582ce8cbd6a5206aee4b59de98c9dfe9b6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 27 Jul 2022 20:19:44 +0200 Subject: DEFINE_SIMPLE_EVENT: support custom JSON keys --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ebc6e6af..4cae2333 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -3294,7 +3294,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) } if (auto* evt = eventCast(event)) - changes |= d->setFullyReadMarker(evt->event_id()); + changes |= d->setFullyReadMarker(evt->eventId()); // For all account data events auto& currentData = d->accountData[event->matrixType()]; -- cgit v1.2.3 From 17cd3beaefa5501a902e08c7644e8cd97c9091a0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 12 Aug 2022 16:46:01 +0200 Subject: Streamline event types This commit introduces a few things to further reduce the boilerplate across event type definitions: - Event type is no more separately stored in Event and therefore no more passed to base event constructors. Until the previous commit, it was used by is() to quickly match the event type; with the new event metatype class, the same is achieved even quicker by comparing metatype pointers. - EventTemplate is a generalisation of StateEvent for all event types providing common constructor signatures and content() for (most) leaf event types. StateEvent therefore has become a partial specialisation of EventTemplate for types derived from StateEventBase; as the known client code base does not use it directly, a compatibility alias is not provided. Also, DEFINE_SIMPLE_EVENT now expands into a class deriving from EventTemplate. - On top of StateEvent->EventTemplate specialisation, KeyedStateEventBase and KeylessStateEventBase types are introduced with appropriate constructor signatures (with or without state_key, respectively) to allow `using` of them from derived event types. To facilitate writing of constraints, concepts for keyed and keyless state event types are also introduced; RoomStateView, e.g., makes use of those to provide appropriate method signatures. - typeId(), unknownEventTypeId(), UnknownEventTypeId are no more provided - they weren't used throughout the known code base (Quaternion, NeoChat), and the concept of "unknown event types" is hereby eliminated entirely. - RoomKeyEvent no more accepts senderId as a parameter; it has never been a good practice as the sender is assigned by Connection anyway. --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 4cae2333..f11b03e1 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2321,7 +2321,7 @@ void Room::setTopic(const QString& newTopic) bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) { - if (le->type() != re->type()) + if (le->metaType() != re->metaType()) return false; if (!re->id().isEmpty()) -- cgit v1.2.3 From 80499cc7619bb857c284e6e89db728ccee9c61f6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 3 Sep 2022 19:12:42 +0200 Subject: More cleanup --- lib/room.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index f11b03e1..042d38ac 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -26,13 +26,13 @@ #include "csapi/inviting.h" #include "csapi/kicking.h" #include "csapi/leaving.h" +#include "csapi/read_markers.h" #include "csapi/receipts.h" #include "csapi/redaction.h" #include "csapi/room_send.h" #include "csapi/room_state.h" #include "csapi/room_upgrades.h" #include "csapi/rooms.h" -#include "csapi/read_markers.h" #include "csapi/tags.h" #include "events/callanswerevent.h" @@ -44,15 +44,15 @@ #include "events/receiptevent.h" #include "events/redactionevent.h" #include "events/roomavatarevent.h" +#include "events/roomcanonicalaliasevent.h" #include "events/roomcreateevent.h" #include "events/roommemberevent.h" +#include "events/roompowerlevelsevent.h" #include "events/roomtombstoneevent.h" #include "events/simplestateevents.h" #include "events/typingevent.h" -#include "events/roompowerlevelsevent.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" -#include "events/roomcanonicalaliasevent.h" #include #include @@ -1353,7 +1353,7 @@ void Room::setTags(TagsMap newTags, ActionScope applyOn) d->setTags(move(newTags)); connection()->callApi( - localUser()->id(), id(), TagEvent::matrixTypeId(), + localUser()->id(), id(), TagEvent::TypeId, TagEvent(d->tags).contentJson()); if (propagate) { @@ -2635,10 +2635,10 @@ RoomEventPtr makeRedacted(const RoomEvent& target, QStringLiteral("membership") }; // clang-format on - static const std::pair keepContentKeysMap[] { - { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, - { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }, - { RoomPowerLevelsEvent::typeId(), + static const std::pair keepContentKeysMap[]{ + { RoomMemberEvent::TypeId, { QStringLiteral("membership") } }, + { RoomCreateEvent::TypeId, { QStringLiteral("creator") } }, + { RoomPowerLevelsEvent::TypeId, { QStringLiteral("ban"), QStringLiteral("events"), QStringLiteral("events_default"), QStringLiteral("kick"), QStringLiteral("redact"), QStringLiteral("state_default"), -- cgit v1.2.3 From fc6321d53f7743f7629841d02c6e28e1a3ab3f83 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 30 Jul 2022 19:57:41 +0200 Subject: RoomStateView::content() This gives a more conventional API compared to queryOr() that can be used for event objects that have content() defined - with the downside being that content() unpacks the entire object instead of retrieving one particular piece (but for state events and single key-value content it's not a problem, and those make for the vast majority of events). --- lib/room.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 042d38ac..a6617cc3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -597,7 +597,7 @@ bool Room::allHistoryLoaded() const QString Room::name() const { - return currentState().queryOr(&RoomNameEvent::name, QString()); + return currentState().content().value; } QStringList Room::aliases() const @@ -613,8 +613,7 @@ QStringList Room::aliases() const QStringList Room::altAliases() const { - return currentState().queryOr(&RoomCanonicalAliasEvent::altAliases, - QStringList()); + return currentState().content().altAliases; } QString Room::canonicalAlias() const -- cgit v1.2.3 From 8e58d28ca0517aeeb43c99bd97ec9ba5ada11c95 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 1 Aug 2022 08:09:40 +0200 Subject: CallEventBase -> CallEvent; pack up all call events These are small enough to comfortably reside in a single translation unit. --- lib/room.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a6617cc3..24939b55 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -35,10 +35,7 @@ #include "csapi/rooms.h" #include "csapi/tags.h" -#include "events/callanswerevent.h" -#include "events/callcandidatesevent.h" -#include "events/callhangupevent.h" -#include "events/callinviteevent.h" +#include "events/callevents.h" #include "events/encryptionevent.h" #include "events/reactionevent.h" #include "events/receiptevent.h" @@ -2916,7 +2913,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (q->supportsCalls()) for (auto it = from; it != syncEdge(); ++it) - if (const auto* evt = it->viewAs()) + if (const auto* evt = it->viewAs()) emit q->callEvent(q, evt); if (totalInserted > 0) { -- cgit v1.2.3 From 7251d6856993a08dd8ec1d4965a310e4cf8e97d3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 10 Aug 2022 21:54:15 +0200 Subject: StateEventBase -> StateEvent Now that StateEvent name is vacated, the naming for event core classes can be completely unified: Event, RoomEvent, CallEvent, StateEvent. --- lib/room.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 24939b55..6bed9b56 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -208,7 +208,7 @@ public: void getPreviousContent(int limit = 10, const QString &filter = {}); - const StateEventBase* getCurrentState(const StateEventKey& evtKey) const + const StateEvent* getCurrentState(const StateEventKey& evtKey) const { const auto* evt = currentState.value(evtKey, nullptr); if (!evt) { @@ -216,9 +216,8 @@ public: // In the absence of a real event, make a stub as-if an event // with empty content has been received. Event classes should be // prepared for empty/invalid/malicious content anyway. - stubbedState.emplace(evtKey, - loadEvent(evtKey.first, - evtKey.second)); + stubbedState.emplace( + evtKey, loadEvent(evtKey.first, evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; qCDebug(STATE) << "Stubbed state size:" << stubbedState.size(); @@ -1565,8 +1564,8 @@ bool Room::usesEncryption() const .isEmpty(); } -const StateEventBase* Room::getCurrentState(const QString& evtType, - const QString& stateKey) const +const StateEvent* Room::getCurrentState(const QString& evtType, + const QString& stateKey) const { return d->getCurrentState({ evtType, stateKey }); } @@ -2279,7 +2278,7 @@ QString Room::postJson(const QString& matrixType, return d->sendEvent(loadEvent(matrixType, eventContent)); } -SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) +SetRoomStateWithKeyJob* Room::setState(const StateEvent& evt) { return setState(evt.matrixType(), evt.stateKey(), evt.contentJson()); } @@ -3095,7 +3094,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // Change the state const auto* const oldStateEvent = - std::exchange(curStateEvent, static_cast(&e)); + std::exchange(curStateEvent, static_cast(&e)); Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); -- cgit v1.2.3 From 715d9e4a858423e8bd9492e3a88d591670349bab Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Jun 2022 15:11:07 +0200 Subject: Room::setTags(): skip full-blown TagEvent creation TagEvent is only created to immediately extract content JSON from it; at the same rate content JSON can be generated directly from content. --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 6bed9b56..24e348e3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1349,7 +1349,7 @@ void Room::setTags(TagsMap newTags, ActionScope applyOn) d->setTags(move(newTags)); connection()->callApi( localUser()->id(), id(), TagEvent::TypeId, - TagEvent(d->tags).contentJson()); + Quotient::toJson(TagEvent::content_type { d->tags })); if (propagate) { for (auto* r = this; (r = r->successor(joinStates));) -- cgit v1.2.3 From eee0b71ebe044a25b8bbe896cb63746599ac0d78 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 17 Sep 2022 19:46:15 +0200 Subject: room.*: fix doc-comments, use [[deprecated]] Previously moc choked on [[attributes]]; deprecated signals, slots etc. were documented as \deprecated instead. By Qt 5.15, that was fixed. --- lib/room.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 24e348e3..1c1aa814 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -771,8 +771,8 @@ Omittable Room::Private::setLastReadReceipt(const QString& userId, // sync, e.g. // TODO: remove in 0.8 if (const auto member = q->user(userId); !isLocalUser(member)) - emit q->readMarkerForUserMoved(member, prevEventId, - storedReceipt.eventId); + QT_IGNORE_DEPRECATIONS(emit q->readMarkerForUserMoved( + member, prevEventId, storedReceipt.eventId)); return prevEventId; } @@ -920,7 +920,8 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) } emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); // TODO: Remove in 0.8 - emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); + QT_IGNORE_DEPRECATIONS( + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId)); return changes; } @@ -1971,7 +1972,8 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) updateDisplayname(); if (changes & Change::PartiallyReadStats) { - emit q->unreadMessagesChanged(q); // TODO: remove in 0.8 + QT_IGNORE_DEPRECATIONS( + emit q->unreadMessagesChanged(q)); // TODO: remove in 0.8 emit q->partiallyReadStatsChanged(); } -- cgit v1.2.3 From 82ca6e81ad5801d4ed2f9445ea15bde4d1344539 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 17 Sep 2022 21:21:35 +0200 Subject: Fix building with GCC --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 1c1aa814..95dd0ab7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -772,7 +772,7 @@ Omittable Room::Private::setLastReadReceipt(const QString& userId, // TODO: remove in 0.8 if (const auto member = q->user(userId); !isLocalUser(member)) QT_IGNORE_DEPRECATIONS(emit q->readMarkerForUserMoved( - member, prevEventId, storedReceipt.eventId)); + member, prevEventId, storedReceipt.eventId);) return prevEventId; } @@ -921,7 +921,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); // TODO: Remove in 0.8 QT_IGNORE_DEPRECATIONS( - emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId)); + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);) return changes; } @@ -1973,7 +1973,7 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) if (changes & Change::PartiallyReadStats) { QT_IGNORE_DEPRECATIONS( - emit q->unreadMessagesChanged(q)); // TODO: remove in 0.8 + emit q->unreadMessagesChanged(q);) // TODO: remove in 0.8 emit q->partiallyReadStatsChanged(); } -- cgit v1.2.3 From 363a7e40e8aa12cb780b076cca8db4f47b70f4fa Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 26 Sep 2022 09:44:20 +0200 Subject: Replace QOlmError with OlmErrorCode QOlmError represents a subset of OlmErrorCode, and the associated fromString() function uses undocumented strings produced inside Olm; meanwhile OlmErrorCode is documented in its own header file. Each QOlm* class now has lastErrorCode() next to lastError() (that, from now, returns a textual representation straight from Olm, not QOlmError enum). Also: including olm/error.h in e2ee/e2ee.h required some rearrangement of the code to make sure non-E2EE configuration still builds. --- lib/room.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 95dd0ab7..382d0243 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -65,7 +65,6 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" #include "e2ee/qolmaccount.h" -#include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" #include "e2ee/qolmutility.h" #include "database.h" -- cgit v1.2.3 From 2ff045d8b381bfbd64100d083f81b61c5fe87b23 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 21 Sep 2022 21:09:49 +0200 Subject: Wrap error reporting into facility macros Facility macros to report Olm errors: QOLM_INTERNAL_ERROR[_X], QOLM_FAIL_OR_LOG[_X] --- lib/room.cpp | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 382d0243..5fdff036 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -357,7 +357,9 @@ public: return false; } - auto megolmSession = QOlmInboundGroupSession::create(sessionKey); + auto expectedMegolmSession = QOlmInboundGroupSession::create(sessionKey); + Q_ASSERT(expectedMegolmSession.has_value()); + auto&& megolmSession = *expectedMegolmSession; if (megolmSession->sessionId() != sessionId) { qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; return false; @@ -442,12 +444,10 @@ public: connection->saveCurrentOutboundMegolmSession( id, *currentOutboundMegolmSession); - const auto sessionKey = currentOutboundMegolmSession->sessionKey(); - if(!sessionKey) { - qCWarning(E2EE) << "Failed to load key for new megolm session"; - return; - } - addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); + currentOutboundMegolmSession->sessionKey(); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), + currentOutboundMegolmSession->sessionKey(), + q->localUser()->id(), "SELF"_ls); } QMultiHash getDevicesWithoutKey() const @@ -460,22 +460,6 @@ public: return connection->database()->devicesWithoutKey( id, devices, currentOutboundMegolmSession->sessionId()); } - - void sendMegolmSession(const QMultiHash& devices) const { - // Save the session to this device - const auto sessionId = currentOutboundMegolmSession->sessionId(); - const auto sessionKey = currentOutboundMegolmSession->sessionKey(); - if(!sessionKey) { - qCWarning(E2EE) << "Error loading session key"; - return; - } - - // Send the session to other people - connection->sendSessionKeyToDevices( - id, sessionId, *sessionKey, devices, - currentOutboundMegolmSession->sessionMessageIndex()); - } - #endif // Quotient_E2EE_ENABLED private: @@ -2029,17 +2013,20 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); } - sendMegolmSession(getDevicesWithoutKey()); + // Send the session to other people + connection->sendSessionKeyToDevices( + id, currentOutboundMegolmSession->sessionId(), + currentOutboundMegolmSession->sessionKey(), getDevicesWithoutKey(), + currentOutboundMegolmSession->sessionMessageIndex()); const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession( id, *currentOutboundMegolmSession); - if(!encrypted) { - qWarning(E2EE) << "Error encrypting message" << encrypted.error(); - return {}; - } - encryptedEvent = makeEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + encryptedEvent = makeEvent( + encrypted, q->connection()->olmAccount()->identityKeys().curve25519, + q->connection()->deviceId(), + currentOutboundMegolmSession->sessionId()); encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); -- cgit v1.2.3 From 388c301222586583a0144f7229a564cf07b597d4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 24 Sep 2022 20:51:00 +0200 Subject: Remove no-op code --- lib/room.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index 5fdff036..0cf818ce 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -444,7 +444,6 @@ public: connection->saveCurrentOutboundMegolmSession( id, *currentOutboundMegolmSession); - currentOutboundMegolmSession->sessionKey(); addInboundGroupSession(currentOutboundMegolmSession->sessionId(), currentOutboundMegolmSession->sessionKey(), q->localUser()->id(), "SELF"_ls); -- cgit v1.2.3