From 896639f1e8b03b5d90e89a28c7d58596eb50df6a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 2 Aug 2020 19:46:59 +0200 Subject: Bump the API version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 808899ba..9d08fff3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() -set(API_VERSION "0.6") +set(API_VERSION "0.7") project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX) option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) -- cgit v1.2.3 From 08e19449ad4e33b9ec3eb66c56501f1c4a977350 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 2 Aug 2020 19:31:22 +0200 Subject: Event::dumpTo: make protected, and RoomEvent override The override adds the event's origin timestamp --- lib/events/event.h | 2 +- lib/events/roomevent.cpp | 6 ++++++ lib/events/roomevent.h | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/events/event.h b/lib/events/event.h index 6c8961ad..5b9f20b7 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -270,10 +270,10 @@ public: virtual bool isStateEvent() const { return false; } virtual bool isCallEvent() const { return false; } - virtual void dumpTo(QDebug dbg) const; protected: QJsonObject& editJson() { return _json; } + virtual void dumpTo(QDebug dbg) const; private: Type _type; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index a59cd6e0..0a4332ad 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -115,6 +115,12 @@ void RoomEvent::addId(const QString& newId) Q_ASSERT(id() == newId); } +void RoomEvent::dumpTo(QDebug dbg) const +{ + Event::dumpTo(dbg); + dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; +} + QJsonObject makeCallContentJson(const QString& callId, int version, QJsonObject content) { diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 621652cb..084cb524 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -87,6 +87,9 @@ public: */ void addId(const QString& newId); +protected: + void dumpTo(QDebug dbg) const override; + private: event_ptr_tt _redactedBecause; }; -- cgit v1.2.3 From 82bcc316d736bd04174876efde38827c34181c33 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 3 Aug 2020 19:10:10 +0200 Subject: Cleanup --- lib/connection.h | 6 ++++-- lib/events/roomcanonicalaliasevent.h | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 48ea4f5e..258280a8 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -238,7 +238,8 @@ public: /** Get the list of rooms with the specified tag */ QVector roomsWithTag(const QString& tagName) const; - /** Mark the room as a direct chat with the user + /*! \brief Mark the room as a direct chat with the user + * * This function marks \p room as a direct chat with \p user. * Emits the signal synchronously, without waiting to complete * synchronisation with the server. @@ -247,7 +248,8 @@ public: */ void addToDirectChats(const Room* room, User* user); - /** Unmark the room from direct chats + /*! \brief Unmark the room from direct chats + * * This function removes the room id from direct chats either for * a specific \p user or for all users if \p user in nullptr. * The room id is used to allow removal of, e.g., ids of forgotten diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index 5d680de7..fadfece0 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -59,12 +59,14 @@ public: : StateEvent(typeId(), obj) { } - explicit RoomCanonicalAliasEvent(const QString& canonicalAlias, const QStringList& altAliases = {}) + explicit RoomCanonicalAliasEvent(const QString& canonicalAlias, + const QStringList& altAliases = {}) : StateEvent(typeId(), matrixTypeId(), QString(), canonicalAlias, altAliases) { } - explicit RoomCanonicalAliasEvent(QString&& canonicalAlias, QStringList&& altAliases = {}) + explicit RoomCanonicalAliasEvent(QString&& canonicalAlias, + QStringList&& altAliases = {}) : StateEvent(typeId(), matrixTypeId(), QString(), std::move(canonicalAlias), std::move(altAliases)) { } -- cgit v1.2.3 From b3ca93a3173af1d16edf5316ede217a202827c01 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 3 Aug 2020 19:13:24 +0200 Subject: quotest: use #test instead of #quotient in loadMembers test #quotient:matrix.org is a primary channel to talk to users and developers, and events produced by changeName test come really noisy. #test:matrix.org is a perfect place to test things out, OTOH. --- tests/quotest.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index ce714a84..622a8cfb 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -305,11 +305,11 @@ void TestManager::doTests() TEST_IMPL(loadMembers) { // Trying to load members from another (larger) room - auto* r = connection()->roomByAlias(QStringLiteral("#quotient:matrix.org"), - JoinState::Join); + const auto& testRoomAlias = QStringLiteral("#test:matrix.org"); + auto* r = connection()->roomByAlias(testRoomAlias, JoinState::Join); if (!r) { - clog << "#quotient:matrix.org is not found in the test user's rooms" - << endl; + clog << testRoomAlias.toStdString() + << " is not found in the test user's rooms" << endl; FAIL_TEST(); } // It's not exactly correct because an arbitrary server might not support -- cgit v1.2.3 From 5319f16ba723e654eaf347f7d5d6957e57bd6d32 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 3 Aug 2020 19:41:58 +0200 Subject: quotest: clean up the room state after testing --- tests/quotest.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 622a8cfb..dc84364f 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -772,6 +772,11 @@ TEST_IMPL(visitResources) void TestManager::conclude() { + // Clean up the room (best effort) + auto* room = testSuite->room(); + room->setTopic({}); + room->localUser()->rename({}); + QString succeededRec { QString::number(succeeded.size()) % " of " % QString::number(succeeded.size() + failed.size() + running.size()) @@ -800,7 +805,6 @@ void TestManager::conclude() // targetRoom->postHtmlText(...) // .then(this, &TestManager::finalize); // Qt-style or // .then([this] { finalize(); }); // STL-style - auto* room = testSuite->room(); auto txnId = room->postHtmlText(plainReport, htmlReport); // Now just wait until all the pending events reach the server connectUntil(room, &Room::messageSent, this, [this, room, plainReport] { -- cgit v1.2.3 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(-) 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 2804099f16a15f6098d26c5d62749d2d1aab9ea2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 13:28:09 +0200 Subject: Cleanup around [BaseJob::]IncorrectResponse[Error] --- lib/jobs/basejob.cpp | 5 ++--- lib/jobs/mediathumbnailjob.cpp | 3 +-- lib/jobs/syncjob.cpp | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 465287c6..d6d48d2f 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -401,9 +401,8 @@ BaseJob::Status BaseJob::Private::parseJson() { QJsonParseError error { 0, QJsonParseError::MissingObject }; jsonResponse = QJsonDocument::fromJson(rawResponse, &error); - return { error.error == QJsonParseError::NoError - ? BaseJob::NoError - : BaseJob::IncorrectResponse, + return { error.error == QJsonParseError::NoError ? NoError + : IncorrectResponse, error.errorString() }; } diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index 33f4236c..a69f00e9 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -53,6 +53,5 @@ BaseJob::Status MediaThumbnailJob::prepareResult() if (_thumbnail.loadFromData(data()->readAll())) return Success; - return { IncorrectResponseError, - QStringLiteral("Could not read image data") }; + return { IncorrectResponse, QStringLiteral("Could not read image data") }; } diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 6b8cfe4b..9087fe50 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -53,9 +53,9 @@ BaseJob::Status SyncJob::prepareResult() { d.parseJson(jsonData()); if (d.unresolvedRooms().isEmpty()) - return BaseJob::Success; + return Success; qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" << d.unresolvedRooms().join(','); - return BaseJob::IncorrectResponseError; + return IncorrectResponse; } -- cgit v1.2.3 From 68a4875c047611e7f58ed001dc9ec4231a11728a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 13:30:21 +0200 Subject: Connection: stop the sync loop on SyncJob::failure --- lib/connection.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 46494a56..4530d95a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -508,7 +508,9 @@ void Connection::sync(int timeout) retriesTaken, nextInMilliseconds); }); connect(job, &SyncJob::failure, this, [this, job] { - d->syncJob = nullptr; + // SyncJob persists with retries on transient errors; if it fails, + // there's likely something serious enough to stop the loop. + stopSync(); if (job->error() == BaseJob::Unauthorised) { qCWarning(SYNCJOB) << "Sync job failed with Unauthorised - login expired?"; @@ -742,7 +744,8 @@ void Connection::stopSync() disconnect(d->syncLoopConnection); if (d->syncJob) // If there's an ongoing sync job, stop it too { - d->syncJob->abandon(); + if (d->syncJob->status().code == BaseJob::Pending) + d->syncJob->abandon(); d->syncJob = nullptr; } } -- cgit v1.2.3 From 7982e62cf53c830b821cbe2d85fd4fef1e633576 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 13:32:57 +0200 Subject: BaseJob: go for a retry on IncorrectResponse The most frequent occurence of IncorrectResponse so far is a proxy/CDN failure. This is not a grave error; there's a chance that the retry will succeed. In the worst case the job will fail after 3 identical errors (except SyncJob that will try to get through forever - but SyncJob failures should still be indicated in the client's UI in some non-intrusive way). --- lib/jobs/basejob.cpp | 55 +++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index d6d48d2f..1f0e84ba 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -586,33 +586,40 @@ void BaseJob::stop() void BaseJob::finishJob() { stop(); - if (error() == TooManyRequests) { + switch(error()) { + case TooManyRequests: emit rateLimited(); d->connection->submit(this); return; - } - if (error() == Unauthorised && !d->needsToken - && !d->connection->accessToken().isEmpty()) { - // Rerun with access token (extension of the spec while - // https://github.com/matrix-org/matrix-doc/issues/701 is pending) - d->connection->setNeedsToken(objectName()); - qCWarning(d->logCat) << this << "re-running with authentication"; - emit retryScheduled(d->retriesTaken, 0); - d->connection->submit(this); - return; - } - if ((error() == NetworkError || error() == Timeout) - && d->retriesTaken < d->maxRetries) { - // TODO: The whole retrying thing should be put to Connection(Manager) - // otherwise independently retrying jobs make a bit of notification - // storm towards the UI. - const seconds retryIn = error() == Timeout ? 0s : getNextRetryInterval(); - ++d->retriesTaken; - qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken - << " in " << retryIn.count() << " s"; - d->retryTimer.start(retryIn); - emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); - return; + case Unauthorised: + if (!d->needsToken && !d->connection->accessToken().isEmpty()) { + // Rerun with access token (extension of the spec while + // https://github.com/matrix-org/matrix-doc/issues/701 is pending) + d->connection->setNeedsToken(objectName()); + qCWarning(d->logCat) << this << "re-running with authentication"; + emit retryScheduled(d->retriesTaken, 0); + d->connection->submit(this); + return; + } + break; + case NetworkError: + case IncorrectResponse: + case Timeout: + if (d->retriesTaken < d->maxRetries) { + // TODO: The whole retrying thing should be put to + // Connection(Manager) otherwise independently retrying jobs make a + // bit of notification storm towards the UI. + const seconds retryIn = error() == Timeout ? 0s + : getNextRetryInterval(); + ++d->retriesTaken; + qCWarning(d->logCat).nospace() + << this << ": retry #" << d->retriesTaken << " in " + << retryIn.count() << " s"; + d->retryTimer.start(retryIn); + emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); + return; + } + default:; } Q_ASSERT(status().code != Pending); -- cgit v1.2.3 From 3f09b1b3e1edcdb7b21d7f3e4f4764f0bd084f30 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 13:52:30 +0200 Subject: quotest: don't fail on past name changes coming in sync Sync may bring past events in a few batches, it turns out, and quotest used to choke on name changes from its own past executions. Also: fix whitespacesmissing ina log line. --- tests/quotest.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index dc84364f..afe26f7a 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -499,17 +499,16 @@ TEST_IMPL(changeName) if (localUser != arrivedUser) return false; - localUser->rename({}); const auto& arrivedNewName = arrivedUser->name(targetRoom); // Old names may diverge e.g. because the original name // hasn't been known to Quotient if (newName == arrivedNewName) FINISH_TEST(true); - clog << "Names mismatch: found" << newName.toStdString() - << "instead of" << arrivedNewName.toStdString() - << endl; - FAIL_TEST(); + clog << "Names mismatch: found " << newName.toStdString() + << " instead of " << arrivedNewName.toStdString() + << "; waiting for the next change" << endl; + return false; }); return false; } -- 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 ++++++++++++++++-------------- lib/room.h | 1 + lib/user.cpp | 277 +++++++++++++---------------------------------------------- lib/user.h | 44 ++++------ 4 files changed, 152 insertions(+), 310 deletions(-) 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)) diff --git a/lib/room.h b/lib/room.h index 1ddff517..7fc03c19 100644 --- a/lib/room.h +++ b/lib/room.h @@ -652,6 +652,7 @@ signals: void userRemoved(Quotient::User* user); void memberAboutToRename(Quotient::User* user, QString newName); void memberRenamed(Quotient::User* user); + void memberAvatarChanged(Quotient::User* user); /// The list of members has changed /** Emitted no more than once per sync, this is a good signal to * for cases when some action should be done upon any change in diff --git a/lib/user.cpp b/lib/user.cpp index 09cf72a2..ffa4efb9 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -38,150 +38,26 @@ #include using namespace Quotient; -using namespace std::placeholders; using std::move; class User::Private { public: - static Avatar makeAvatar(QUrl url) { return Avatar(move(url)); } - Private(QString userId) : id(move(userId)), hueF(stringToHueF(id)) { } QString id; - - QString mostUsedName; - QMultiHash otherNames; qreal hueF; - Avatar mostUsedAvatar { makeAvatar({}) }; - std::vector otherAvatars; - auto otherAvatar(const QUrl& url) - { - return std::find_if(otherAvatars.begin(), otherAvatars.end(), - [&url](const auto& av) { return av.url() == url; }); - } - QMultiHash avatarsToRooms; - - mutable int totalRooms = 0; - QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, const QString& oldName); - QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; - void setAvatarForRoom(const Room* r, const QUrl& newUrl, const QUrl& oldUrl); - - void setAvatarOnServer(QString contentUri, User* q); + QString defaultName; + Avatar defaultAvatar; + // NB: This container is ever-growing. Even if the user no more scrolls + // the timeline that far back, historical avatars are still kept around. + // This is consistent with the rest of Quotient, as room timelines + // are never vacuumed either. This will probably change in the future. + /// Map of mediaId to Avatar objects + static UnorderedMap otherAvatars; }; -QString User::Private::nameForRoom(const Room* r, const QString& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (!hint.isNull() && (hint == mostUsedName || otherNames.contains(hint, r))) - return hint; - return otherNames.key(r, mostUsedName); -} - -static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; - -void User::Private::setNameForRoom(const Room* r, QString newName, - const QString& oldName) -{ - Q_ASSERT(oldName != newName); - Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); - if (totalRooms < 2) { - Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedName = move(newName); - return; - } - otherNames.remove(oldName, r); - if (newName != mostUsedName) { - // Check if the newName is about to become most used. - if (otherNames.count(newName) >= totalRooms - otherNames.size()) { - Q_ASSERT(totalRooms > 1); - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) { - qCDebug(MAIN) << "Switching the most used name of user" << id - << "from" << mostUsedName << "to" << newName; - qCDebug(MAIN) << "The user is in" << totalRooms << "rooms"; - et.start(); - } - - for (auto* r1: r->connection()->allRooms()) - if (nameForRoom(r1) == mostUsedName) - otherNames.insert(mostUsedName, r1); - - mostUsedName = newName; - otherNames.remove(newName); - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used name"; - } else - otherNames.insert(newName, r); - } -} - -QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) - return hint; - auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); - return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); -} - -void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl) -{ - Q_ASSERT(oldUrl != newUrl); - Q_ASSERT(oldUrl == mostUsedAvatar.url() - || avatarsToRooms.contains(oldUrl, r)); - if (totalRooms < 2) { - Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedAvatar.updateUrl(newUrl); - return; - } - avatarsToRooms.remove(oldUrl, r); - if (!avatarsToRooms.contains(oldUrl)) { - auto it = otherAvatar(oldUrl); - if (it != otherAvatars.end()) - otherAvatars.erase(it); - } - if (newUrl != mostUsedAvatar.url()) { - // Check if the new avatar is about to become most used. - const auto newUrlUsage = avatarsToRooms.count(newUrl); - if (newUrlUsage >= totalRooms - avatarsToRooms.size()) { - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) { - qCInfo(MAIN) - << "Switching the most used avatar of user" << id - << "from" << mostUsedAvatar.url().toDisplayString() << "to" - << newUrl.toDisplayString(); - et.start(); - } - avatarsToRooms.remove(newUrl); - auto nextMostUsedIt = otherAvatar(newUrl); - if (nextMostUsedIt == otherAvatars.end()) { - qCCritical(MAIN) - << id << "doesn't have" << newUrl.toDisplayString() - << "in otherAvatars though it seems to be used in" - << newUrlUsage << "rooms"; - Q_ASSERT(false); - otherAvatars.emplace_back(makeAvatar(newUrl)); - nextMostUsedIt = otherAvatars.end() - 1; - } - std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: r->connection()->allRooms()) - if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) - avatarsToRooms.insert(nextMostUsedIt->url(), r1); - - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used avatar"; - } else { - if (otherAvatar(newUrl) == otherAvatars.end()) - otherAvatars.emplace_back(makeAvatar(newUrl)); - avatarsToRooms.insert(newUrl, r); - } - } -} +decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; User::User(QString userId, Connection* connection) : QObject(connection), d(new Private(move(userId))) @@ -210,43 +86,31 @@ bool User::isGuest() const int User::hue() const { return int(hueF() * 359); } -QString User::name(const Room* room) const { return d->nameForRoom(room); } - -QString User::rawName(const Room* room) const { return name(room); } - -void User::updateName(const QString& newName, const Room* room) +QString User::name(const Room* room) const { - updateName(newName, d->nameForRoom(room), room); + return room ? room->getCurrentState(id())->displayName() + : d->defaultName; } -void User::updateName(const QString& newName, const QString& oldName, - const Room* room) -{ - Q_ASSERT(oldName == d->mostUsedName - || d->otherNames.contains(oldName, room)); - if (newName != oldName) { - emit nameAboutToChange(newName, oldName, room); - d->setNameForRoom(room, newName, oldName); - emit nameChanged(newName, oldName, room); - } -} - -void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room) -{ - Q_ASSERT(oldUrl == d->mostUsedAvatar.url() - || d->avatarsToRooms.contains(oldUrl, room)); - if (newUrl != oldUrl) { - d->setAvatarForRoom(room, newUrl, oldUrl); - emit avatarChanged(this, room); - } -} +QString User::rawName(const Room* room) const { return name(room); } void User::rename(const QString& newName) { const auto actualNewName = sanitized(newName); + if (actualNewName == d->defaultName) + return; // Nothing to do + connect(connection()->callApi(id(), actualNewName), - &BaseJob::success, this, [=] { updateName(actualNewName); }); + &BaseJob::success, this, [this, actualNewName] { + // Check again, it could have changed meanwhile + if (actualNewName != d->defaultName) { + d->defaultName = actualNewName; + emit defaultNameChanged(); + } else + qCWarning(MAIN) + << "User" << id() << "already has profile name set to" + << actualNewName; + }); } void User::rename(const QString& newName, const Room* r) @@ -262,22 +126,37 @@ void User::rename(const QString& newName, const Room* r) const auto actualNewName = sanitized(newName); MemberEventContent evtC; evtC.displayName = actualNewName; - connect(r->setState(id(), move(evtC)), &BaseJob::success, - this, [=] { updateName(actualNewName, r); }); + r->setState(id(), move(evtC)); + // The state will be updated locally after it arrives with sync +} + +template +inline bool User::doSetAvatar(SourceT&& source) +{ + return d->defaultAvatar.upload( + connection(), source, [this](const QString& contentUri) { + auto* j = connection()->callApi(id(), contentUri); + connect(j, &BaseJob::success, this, + [this, newUrl = QUrl(contentUri)] { + if (newUrl == d->defaultAvatar.url()) { + d->defaultAvatar.updateUrl(move(newUrl)); + emit defaultAvatarChanged(); + } else + qCWarning(MAIN) << "User" << id() + << "already has avatar URL set to" + << newUrl.toDisplayString(); + }); + }); } bool User::setAvatar(const QString& fileName) { - return avatarObject().upload(connection(), fileName, - std::bind(&Private::setAvatarOnServer, - d.data(), _1, this)); + return doSetAvatar(fileName); } bool User::setAvatar(QIODevice* source) { - return avatarObject().upload(connection(), source, - std::bind(&Private::setAvatarOnServer, - d.data(), _1, this)); + return doSetAvatar(source); } void User::requestDirectChat() { connection()->requestDirectChat(this); } @@ -288,34 +167,29 @@ void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(this); } bool User::isIgnored() const { return connection()->isIgnored(this); } -void User::Private::setAvatarOnServer(QString contentUri, User* q) -{ - auto* j = q->connection()->callApi(id, contentUri); - connect(j, &BaseJob::success, q, - [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); -} - QString User::displayname(const Room* room) const { - if (room) - return room->roomMembername(this); - - const auto name = d->nameForRoom(nullptr); - return name.isEmpty() ? d->id : name; + return room ? room->roomMembername(this) + : d->defaultName.isEmpty() ? d->id + : d->defaultName; } QString User::fullName(const Room* room) const { - const auto name = d->nameForRoom(room); - return name.isEmpty() ? id() : name % " (" % id() % ')'; + const auto displayName = name(room); + return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } QString User::bridged() const { return {}; } const Avatar& User::avatarObject(const Room* room) const { - auto it = d->otherAvatar(d->avatarUrlForRoom(room)); - return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; + if (!room) + return d->defaultAvatar; + + const auto& url = room->getCurrentState(id())->avatarUrl(); + const auto& mediaId = url.authority() + url.path(); + return d->otherAvatars.try_emplace(mediaId, url).first->second; } QImage User::avatar(int dimension, const Room* room) @@ -331,10 +205,7 @@ QImage User::avatar(int width, int height, const Room* room) QImage User::avatar(int width, int height, const Room* room, const Avatar::get_callback_t& callback) { - return avatarObject(room).get(connection(), width, height, [=] { - emit avatarChanged(this, room); - callback(); - }); + return avatarObject(room).get(connection(), width, height, callback); } QString User::avatarMediaId(const Room* room) const @@ -347,30 +218,4 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room, - bool firstMention) -{ - Q_ASSERT(room); - - if (firstMention) - ++d->totalRooms; - - if (event.membership() != MembershipType::Invite - && event.membership() != MembershipType::Join) - return; - - if (event.prevContent()) { - // FIXME: the hint doesn't work for bridged users - auto oldNameHint = d->nameForRoom(room, - event.prevContent()->displayName); - updateName(event.displayName(), oldNameHint, room); - updateAvatarUrl(event.avatarUrl(), - d->avatarUrlForRoom(room, event.prevContent()->avatarUrl), - room); - } else { - updateName(event.displayName(), room); - updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room), room); - } -} - qreal User::hueF() const { return d->hueF; } diff --git a/lib/user.h b/lib/user.h index e4328f1d..a3b22480 100644 --- a/lib/user.h +++ b/lib/user.h @@ -33,13 +33,12 @@ class User : public QObject { Q_PROPERTY(bool isGuest READ isGuest CONSTANT) Q_PROPERTY(int hue READ hue CONSTANT) Q_PROPERTY(qreal hueF READ hueF CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged - STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + Q_PROPERTY(QString name READ name NOTIFY defaultNameChanged) + Q_PROPERTY(QString displayName READ displayname NOTIFY defaultNameChanged STORED false) + Q_PROPERTY(QString fullName READ fullName NOTIFY defaultNameChanged STORED false) + Q_PROPERTY(QString bridgeName READ bridged NOTIFY defaultNameChanged STORED false) + Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) public: User(QString userId, Connection* connection); ~User() override; @@ -123,20 +122,14 @@ public: QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; - /// This method is for internal use and should not be called - /// from client code - // FIXME: Move it away to private - void processEvent(const RoomMemberEvent& event, const Room* r, - bool firstMention); - public slots: - /** Set a new name in the global user profile */ + /// Set a new name in the global user profile void rename(const QString& newName); - /** Set a new name for the user in one room */ + /// Set a new name for the user in one room void rename(const QString& newName, const Room* r); - /** Upload the file and use it as an avatar */ + /// Upload the file and use it as an avatar bool setAvatar(const QString& fileName); - /** Upload contents of the QIODevice and set that as an avatar */ + /// Upload contents of the QIODevice and set that as an avatar bool setAvatar(QIODevice* source); /// Create or find a direct chat with this user /*! The resulting chat is returned asynchronously via @@ -151,21 +144,14 @@ public slots: bool isIgnored() const; signals: - void nameAboutToChange(QString newName, QString oldName, - const Quotient::Room* roomContext); - void nameChanged(QString newName, QString oldName, - const Quotient::Room* roomContext); - void avatarChanged(Quotient::User* user, const Quotient::Room* roomContext); - -private slots: - void updateName(const QString& newName, const Room* room = nullptr); - void updateName(const QString& newName, const QString& oldName, - const Room* room = nullptr); - void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room = nullptr); + void defaultNameChanged(); + void defaultAvatarChanged(); private: class Private; QScopedPointer d; + + template + bool doSetAvatar(SourceT&& source); }; } // namespace Quotient -- cgit v1.2.3 From 13153e65276ae35cbfd02fdbb120c44f4051a2aa Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 17:20:32 +0200 Subject: Connection: self-delete after emitting loggedOut() The Connection object has quite few uses after logging out - neither rooms nor users under it no more represent actual situation, and the object cannot be cleanly reused for a new login (also, the use case for that is pretty dubious). This doesn't cover the case when the session has been forcibly logged-out by the server (causing loginError() to be emitted) - in that case re-authentication is an expected flow. --- lib/connection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4530d95a..97805fe7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -471,6 +471,7 @@ void Connection::logout() disconnect(d->syncLoopConnection); d->data->setToken({}); emit loggedOut(); + deleteLater(); } else { // logout() somehow didn't proceed - restore the session state emit stateChanged(); if (wasSyncing) -- cgit v1.2.3 From 68b9c6d5388fd21f761a6fdc4960a26d70a36b0b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 4 Aug 2020 18:10:57 +0200 Subject: quotest: fix the log wording --- tests/quotest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index afe26f7a..0ad5ff6b 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -507,7 +507,7 @@ TEST_IMPL(changeName) clog << "Names mismatch: found " << newName.toStdString() << " instead of " << arrivedNewName.toStdString() - << "; waiting for the next change" << endl; + << "; waiting for the next event" << endl; return false; }); return false; -- cgit v1.2.3 From 08b9adcf611e0307daa8297205eb9ed3af66c71e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 11 Aug 2020 22:53:58 +0200 Subject: Drop unneeded #include --- lib/events/accountdataevents.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index a55016d9..0f240aa1 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -18,7 +18,6 @@ #pragma once -#include "converters.h" #include "event.h" #include "eventcontent.h" -- cgit v1.2.3 From 8fa7dae82ac8e49b2a965377a663aedd08a5230b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 11 Aug 2020 23:02:51 +0200 Subject: Build system optimisations * -DQT_NO_JAVA_STYLE_ITERATORS * Use precompiled headers on CMake >= 3.16 (except GCC, blame its cheap PCH implementation) * -fvisibility-inlines-hidden when using CMake --- CMakeLists.txt | 16 +++++++++++----- libquotient.pri | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d08fff3..d3bede48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,11 +49,12 @@ if (MSVC) /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() - foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter - no-gnu-zero-variadic-macro-arguments) - CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) - if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") - add_compile_options(-W${FLAG}) + foreach (FLAG Wall W Wpedantic Wextra Werror=return-type Wno-unused-parameter + Wno-gnu-zero-variadic-macro-arguments fvisibility-inlines-hidden) + CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) + if ( COMPILER_${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") + add_compile_options(-${FLAG}) endif () endforeach () endif() @@ -257,6 +258,11 @@ file(GLOB_RECURSE api_SRCS ${add_CONFIGURE_DEPENDS} ${FULL_CSAPI_DIR}/*.cpp) set(tests_SRCS tests/quotest.cpp) add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) +target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS) +if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" + AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 + target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) +endif () if (${PROJECT_NAME}_ENABLE_E2EE) target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_E2EE_ENABLED) endif() diff --git a/libquotient.pri b/libquotient.pri index 98fe3b03..ef0f112a 100644 --- a/libquotient.pri +++ b/libquotient.pri @@ -13,6 +13,7 @@ win32-msvc* { QMAKE_CXXFLAGS_WARN_ON *= -Wno-unused-parameter } +DEFINES += QT_NO_JAVA_STYLE_ITERATORS contains(DEFINES, Quotient_E2EE_ENABLED=.) { contains(DEFINES, USE_INTREE_LIBQOLM=.) { include(3rdparty/libQtOlm/libQtOlm.pri) -- 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/qt_connection_util.h | 2 +- lib/room.cpp | 4 ++-- lib/util.h | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 04dc6b27..699735d4 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -46,7 +46,7 @@ namespace _impl { // arguments are always copied (at best - COWed) to the context of // the slot. Therefore the slot decorator receives const ArgTs&... // rather than ArgTs&&... - // TODO: std::bind_front() instead of lambda. + // TODO (C++20): std::bind_front() instead of lambda. c = QObject::connect(sender, signal, context, [pc = std::move(pc), decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) { 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); } diff --git a/lib/util.h b/lib/util.h index 943ff8db..8c92df74 100644 --- a/lib/util.h +++ b/lib/util.h @@ -200,6 +200,10 @@ using fn_arg_t = // TODO: get rid of it as soon as Apple Clang gets proper deduction guides // for std::function<> +// ...or consider using QtPrivate magic used by QObject::connect() +// since wrap_in_function() is actually made for qt_connection_util.h +// ...for inspiration, also check a possible std::not_fn implementation at +// https://en.cppreference.com/w/cpp/utility/functional/not_fn template inline auto wrap_in_function(FnT&& f) { @@ -217,7 +221,7 @@ inline auto operator"" _ls(const char* s, std::size_t size) */ template class Range { - // Looking forward to C++23 ranges + // Looking forward to C++20 ranges using iterator = typename ArrayT::iterator; using const_iterator = typename ArrayT::const_iterator; using size_type = typename ArrayT::size_type; -- cgit v1.2.3 From 90e940c9b398aa26d50095b2ca6905d0815f6412 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 22 Aug 2020 21:39:31 +0200 Subject: quotest: fix changeName test failures Member renames upon profile changes don't come right away, it turns out; so check User::nameChanged instead of Room::memberRenamed. --- tests/quotest.cpp | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 0ad5ff6b..f14edcc0 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -493,22 +493,14 @@ TEST_IMPL(changeName) { auto* const localUser = connection()->user(); const auto& newName = connection()->generateTxnId(); // See setTopic() + clog << "Renaming the user to " << newName.toStdString() << endl; localUser->rename(newName); - connectUntil(targetRoom, &Room::memberRenamed, this, - [this, thisTest, newName, localUser](const User* arrivedUser) { - if (localUser != arrivedUser) + connectUntil(localUser, &User::nameChanged, this, + [this, thisTest, newName](const QString& emittedName, QString, + const Room* r) { + if (r != nullptr) return false; - - const auto& arrivedNewName = arrivedUser->name(targetRoom); - // Old names may diverge e.g. because the original name - // hasn't been known to Quotient - if (newName == arrivedNewName) - FINISH_TEST(true); - - clog << "Names mismatch: found " << newName.toStdString() - << " instead of " << arrivedNewName.toStdString() - << "; waiting for the next event" << endl; - return false; + FINISH_TEST(emittedName == newName); }); return false; } -- cgit v1.2.3 From 1cf07ee56315af86cadccc977948e9ed1d51da1a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 22 Aug 2020 22:34:07 +0200 Subject: quotest: fix FTBFS after a sloppy cherry-pick --- tests/quotest.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index f14edcc0..3a32da69 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -495,12 +495,9 @@ TEST_IMPL(changeName) const auto& newName = connection()->generateTxnId(); // See setTopic() clog << "Renaming the user to " << newName.toStdString() << endl; localUser->rename(newName); - connectUntil(localUser, &User::nameChanged, this, - [this, thisTest, newName](const QString& emittedName, QString, - const Room* r) { - if (r != nullptr) - return false; - FINISH_TEST(emittedName == newName); + connectUntil(localUser, &User::defaultNameChanged, this, + [this, thisTest, localUser, newName] { + FINISH_TEST(localUser->name() == newName); }); return false; } -- cgit v1.2.3 From d7820ada21fb29b273a68a5ea69477ef5763d5de Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 23 Aug 2020 10:28:59 +0200 Subject: More cleanup; drop Qt bearer management on Qt 5.15+ Qt 5.15 deprecates bearer management. --- lib/networkaccessmanager.cpp | 5 +++-- lib/util.cpp | 30 +++++++++--------------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 8ee080bf..b9037bcc 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -50,11 +50,12 @@ void NetworkAccessManager::clearIgnoredSslErrors() static NetworkAccessManager* createNam() { auto nam = new NetworkAccessManager(QCoreApplication::instance()); - // See #109. Once Qt bearer management gets better, this workaround - // should become unnecessary. +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + // See #109; in newer Qt, bearer management is deprecated altogether nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); }); +#endif return nam; } diff --git a/lib/util.cpp b/lib/util.cpp index 61661de8..797f6d69 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -126,32 +126,30 @@ QString Quotient::serverPart(const QString& mxId) % ServerPartRegEx % ")$"; static QRegularExpression parser( re, - QRegularExpression::UseUnicodePropertiesOption); // Because Asian - // digits + QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits return parser.match(mxId).captured(1); } // Tests for function_traits<> -#ifdef Q_CC_CLANG -# pragma clang diagnostic push -# pragma ide diagnostic ignored "OCSimplifyInspection" -#endif using namespace Quotient; -int f(); -static_assert(std::is_same, int>::value, +int f_(); +static_assert(std::is_same, int>::value, "Test fn_return_t<>"); -void f1(int, QString); -static_assert(std::is_same, QString>::value, +void f1_(int, QString); +static_assert(std::is_same, QString>::value, "Test fn_arg_t<>"); struct Fo { int operator()(); + static constexpr auto l = [] { return 0.0f; }; }; static_assert(std::is_same, int>::value, "Test return type of function object"); +static_assert(std::is_same, float>::value, + "Test return type of lambda"); struct Fo1 { void operator()(int); @@ -159,20 +157,10 @@ struct Fo1 { static_assert(std::is_same, int>(), "Test fn_arg_t defaulting to first argument"); -#if (!defined(_MSC_VER) || _MSC_VER >= 1910) -static auto l = [] { return 1; }; -static_assert(std::is_same, int>::value, - "Test fn_return_t<> with lambda"); -#endif - template -QString ft(T&&) +static QString ft(T&&) { return {}; } static_assert(std::is_same)>, QString&&>(), "Test function templates"); - -#ifdef Q_CC_CLANG -# pragma clang diagnostic pop -#endif -- cgit v1.2.3 From c641e3f070d6687e10a2cdb9fe3e4f7d12c5704f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 4 Sep 2020 22:45:04 +0200 Subject: csapi/profile.*: require displayname/avatar_url See https://github.com/matrix-org/matrix-doc/issues/2717 --- lib/csapi/profile.cpp | 4 ++-- lib/csapi/profile.h | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index cb8f72be..8436b8e6 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -15,7 +15,7 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId, % "/displayname") { QJsonObject _data; - addParam(_data, QStringLiteral("displayname"), displayname); + addParam<>(_data, QStringLiteral("displayname"), displayname); setRequestData(std::move(_data)); } @@ -39,7 +39,7 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl % "/avatar_url") { QJsonObject _data; - addParam(_data, QStringLiteral("avatar_url"), avatarUrl); + addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); setRequestData(std::move(_data)); } diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 88bf3166..3858fab2 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -24,7 +24,7 @@ public: * The new display name for this user. */ explicit SetDisplayNameJob(const QString& userId, - const QString& displayname = {}); + const QString& displayname); }; /*! \brief Get the user's display name. @@ -73,8 +73,7 @@ public: * \param avatarUrl * The new avatar URL for this user. */ - explicit SetAvatarUrlJob(const QString& userId, - const QString& avatarUrl = {}); + explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl); }; /*! \brief Get the user's avatar URL. -- cgit v1.2.3 From 65c60c786dd18553a804e01ac60e5a167cb0a400 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Sep 2020 22:32:33 +0200 Subject: SyndData::parseJson: use fromJson() ...instead of a complicated explicit code converting from JSON to varianthash to hash. --- lib/syncdata.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index a9ec5ee7..e6472e18 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -182,12 +182,8 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) accountData = load(json, "account_data"_ls); toDeviceEvents = load(json, "to_device"_ls); - auto deviceOneTimeKeysCountVariantHash = - json.value("device_one_time_keys_count"_ls).toObject().toVariantHash(); - for (auto key : deviceOneTimeKeysCountVariantHash.keys()) { - deviceOneTimeKeysCount_.insert( - key, deviceOneTimeKeysCountVariantHash.value(key).toInt()); - } + fromJson(json.value("device_one_time_keys_count"_ls), + deviceOneTimeKeysCount_); auto rooms = json.value("rooms"_ls).toObject(); JoinStates::Int ii = 1; // ii is used to make a JoinState value -- 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/avatar.cpp | 4 ++-- lib/connection.cpp | 7 ++++--- lib/connection.h | 2 +- lib/room.cpp | 15 +++++++++------ lib/settings.cpp | 11 ++++++----- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/avatar.cpp b/lib/avatar.cpp index cb734984..c65aa25c 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -45,7 +45,7 @@ public: QImage get(Connection* connection, QSize size, get_callback_t callback) const; - bool upload(UploadContentJob* job, upload_callback_t callback); + bool upload(UploadContentJob* job, upload_callback_t&& callback); bool checkUrl(const QUrl& url) const; QString localFile() const; @@ -154,7 +154,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size, return result; } -bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) +bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback) { _uploadRequest = job; if (!isJobRunning(_uploadRequest)) diff --git a/lib/connection.cpp b/lib/connection.cpp index 97805fe7..d3324d47 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -931,7 +931,7 @@ void Connection::doInDirectChat(User* u, // There can be more than one DC; find the first valid (existing and // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; - for (auto it = d->directChats.find(u); + for (auto it = std::as_const(d->directChats).find(u); it != d->directChats.end() && it.key() == u; ++it) { const auto& roomId = *it; if (auto r = room(roomId, JoinState::Join)) { @@ -1232,7 +1232,7 @@ int Connection::roomsCount(JoinStates joinStates) const { // Using int to maintain compatibility with QML // (consider also that QHash<>::size() returns int anyway). - return int(std::count_if(d->roomMap.begin(), d->roomMap.end(), + return int(std::count_if(d->roomMap.cbegin(), d->roomMap.cend(), [joinStates](Room* r) { return joinStates.testFlag(r->joinState()); })); @@ -1297,7 +1297,8 @@ QStringList Connection::tagNames() const QVector Connection::roomsWithTag(const QString& tagName) const { QVector rooms; - std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), + std::copy_if(d->roomMap.cbegin(), d->roomMap.cend(), + std::back_inserter(rooms), [&tagName](Room* r) { return r->tags().contains(tagName); }); return rooms; } diff --git a/lib/connection.h b/lib/connection.h index 258280a8..6517b909 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -326,7 +326,7 @@ public: const QStringList& previousRoomAliases, const QStringList& roomAliases); Q_INVOKABLE Quotient::Room* invitation(const QString& roomId) const; - Q_INVOKABLE Quotient::User* user(const QString& userId); + Q_INVOKABLE Quotient::User* user(const QString& uId); const User* user() const; User* user(); QString userId() const; 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()) { diff --git a/lib/settings.cpp b/lib/settings.cpp index 5413693d..dd086d9c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -53,11 +53,12 @@ bool Settings::contains(const QString& key) const QStringList Settings::childGroups() const { - auto l = QSettings::childGroups(); - for (const auto& g: legacySettings.childGroups()) - if (!l.contains(g)) - l.push_back(g); - return l; + auto groups = QSettings::childGroups(); + const auto& legacyGroups = legacySettings.childGroups(); + for (const auto& g: legacyGroups) + if (!groups.contains(g)) + groups.push_back(g); + return groups; } void SettingsGroup::setValue(const QString& key, const QVariant& value) -- cgit v1.2.3 From 6c91fa90ced57ca3067993add2ac03a31403150e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 4 Sep 2020 07:52:06 +0200 Subject: CONTRIBUTING.md: clarify the matrix-doc repo situation [skip ci] --- CONTRIBUTING.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd604621..edc06ef7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -310,9 +310,11 @@ Because before both original authors of libQuotient had to do monkey business of (you might need to pass `-DCMAKE_PREFIX_PATH=`, similar to libQuotient itself). 3. Get the Matrix CS API definitions that are included in the matrix-doc repo: - `git clone https://github.com/quotient-im/matrix-doc.git` - (quotient-im/matrix-doc is a fork that's known to produce working code; - you may want to use your own fork if you wish to alter something in the API). + `git clone https://github.com/quotient-im/matrix-doc.git`. + The fork at `quotient-im/matrix-doc` is closely following the official + repo (`matrix-org/matrix-doc`), curating commits that are known to produce + working code for Quotient. You may want to use your own fork if you wish + to alter something in the API. 4. If you plan to submit a PR or just would like the generated code to be formatted, you should either ensure you have clang-format (version 6 at least) in your PATH or pass the _absolute_ path to it by adding @@ -320,7 +322,7 @@ Because before both original authors of libQuotient had to do monkey business of #### Generating CS API contents 1. Pass additional configuration to CMake when configuring libQuotient: - `-DMATRIX_DOC_PATH= -DGTAD_PATH=`. + `-DMATRIX_DOC_PATH= -DGTAD_PATH=`. If everything's right, these two CMake variables will be mentioned in CMake output and will trigger configuration of an additional build target, see the next step. -- cgit v1.2.3 From 6e4e22cfa4a807ab18fbf1d704f312d0876a4ef5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 4 Sep 2020 21:41:48 +0200 Subject: Update documentation [skip ci] --- CONTRIBUTING.md | 175 +++++++++++++++++++++++++++++++++++++++----------------- README.md | 21 +++++-- SECURITY.md | 22 ++++--- 3 files changed, 152 insertions(+), 66 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edc06ef7..281ab5b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ For general discussion, feel free to use our Matrix room: If you're new to the project (or FLOSS in general), [issues tagged as easy](https://github.com/quotient-im/libQuotient/labels/easy) -are smaller tasks that may typically take 1-3 days. +are smaller tasks that don't require much knowledge about the project. You are welcome aboard! ### Pull requests and different branches recommended @@ -134,7 +134,8 @@ is not an option, use <br /> (an HTML break). ## End of TL;DR -If you don't plan/have substantial contributions, you can end reading here. Further sections are for those who's going to actively hack on the library code. +If you don't plan/have substantial contributions, you can stop reading here. +Further sections are for those who's going to actively hack on the library code. ## Code changes @@ -154,7 +155,7 @@ restrictions, notably: * enumerators and slots cannot have `[[attributes]]` because moc of Qt 5.9 chokes on them. This will be lifted when we move on to Qt 5.12 for the oldest supported version. -* things from `std::filesystem` cannot be used yet until we push the oldest +* things from `std::filesystem` cannot be used until we push the oldest required g++/libc to version 8. The code style is defined by `.clang-format`, and in general, all C++ files @@ -211,27 +212,25 @@ implementation details as much as possible. `_impl` namespace is reserved for definitions that should not be used by clients and are not covered by API guarantees. -Note: As of now, all header files of libQuotient are considered public; this may change eventually. +Note: As of now, all header files of libQuotient are considered public; +this may change eventually. ### Comments Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. -Doxygen style is preferred; but JavaDoc is acceptable too. Some parts are +Doxygen style is preferred; but Javadoc is acceptable too. Some parts are not documented at all; adding doc-comments to them is highly encouraged. -Doc-comments for summaries should be separate from those details. Either of -the two following ways is fine, with considerable preference on the first one: -1. Use `///` for the summary comment and `/*! ... */` for details. -2. Use `\brief` (or `@brief`) for the summary, and follow with details after - an empty doc-comment line. You can use either of the delimiters in that case. +Use `\brief` for the summary, and follow with details after +an empty doc-comment line. -In the code, the advice for commenting is as follows: +For in-code comments, the advice is as follows: * Don't restate what's happening in the code unless it's not really obvious. We assume the readers to have at least some command of C++ and Qt. If your code is not obvious, consider rewriting it for clarity. * Both C++ and Qt still come with their arcane features and dark corners, - and we don't want to limit anybody who'd feels they have a case for + and we don't want to limit anybody who feels they have a case for variable templates, raw literals, or use `qAsConst` to avoid container detachment. Use your experience to figure what might be less well-known to readers and comment such cases (references to web pages, Quotient wiki etc. @@ -266,42 +265,77 @@ tests can go async, which is the biggest hurdle for Qt Test adoption. ### Security, privacy, and performance -Pay attention to security, and work *with* (not against) the usual security hardening mechanisms (however few in C++). +Pay attention to security, and work *with*, not against, the usual security hardening practices (however few in C++). -`char *` and similar unchecked C-style read/write arrays are forbidden - use Qt containers or at the very least `std::array<>` instead. Where you see fit (usually with data structures), try to use smart pointers, especially `std::unique_ptr<>` or `QScopedPointer` instead of bare pointers. When dealing with `QObject`s, use the parent-child ownership semantics exercised by Qt (this is preferred to using smart pointers). Shared pointers are not used in the code so far; but if you find a particular use case where the strict semantic of unique pointers doesn't help and a shared pointer is necessary, feel free to step up with the working code and it will be considered for inclusion. +`char *` and similar unchecked C-style read/write arrays are forbidden - use +Qt containers or at the very least `std::array<>` instead. Where you see fit +(usually with data structures), try to use smart pointers, especially +`std::unique_ptr<>` or `QScopedPointer` instead of bare pointers. When dealing +with `QObject`s, use the parent-child ownership semantics exercised by Qt +(this is preferred to using smart pointers). If you find a particular use case +where the strict semantic of unique pointers doesn't help and a shared pointer +is necessary, feel free to step up with the working code and it will be +considered for inclusion. Exercise the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) where reasonable and appropriate. Prefer less-coupled cohesive code. -Protect private information, in particular passwords and email addresses. Absolutely _don't_ spill around this information in logs - use `access_token` and similar opaque ids instead, and only display those in UI where needed. Do not forget about local access to data (in particular, be very careful when storing something in temporary files, let alone permanent configuration or state). Avoid mechanisms that could be used for tracking where possible (we do need to verify people are logged in but that's pretty much it), and ensure that third parties can't use interactions for tracking. Matrix protocols evolve towards decoupling the personally identifiable information from user activity entirely - follow this trend. - -We want the software to have decent performance for typical users. At the same time we keep libQuotient single-threaded as much as possible, to keep the code simple. That means being cautious about operation complexity (read about big-O notation if you need a kickstart on the topic). This especially refers to operations on the whole timeline and the list of users - each of these can have tens of thousands of elements so even operations with linear complexity, if heavy enough, can produce noticeable GUI freezing. When you don't see a way to reduce algorithmic complexity, embed occasional `processEvents()` invocations in heavy loops (see `Connection::saveState()` to get the idea). +Protect private information, in particular passwords and email addresses. +Absolutely _don't_ spill around this information in logs, and only display +those in UI where really needed. Do not forget about local access to data +(in particular, be very careful when storing something in temporary files, +let alone permanent configuration or state). Avoid mechanisms that could be +used for tracking where possible (we do need to verify people are logged in +but that's pretty much it), and ensure that third parties can't use interactions +for tracking. Matrix protocols evolve towards decoupling +the personally identifiable information from user activity entirely - follow +this trend. + +We want the software to have decent performance for users even on weaker +machines. At the same time we keep libQuotient single-threaded as much +as possible, to keep the code simple. That means being cautious about operation +complexity (read about big-O notation if you need a kickstart on the topic). +This especially refers to operations on the whole timeline and the list of +users - each of these can have tens of thousands of elements so even operations +with linear complexity, if heavy enough (with I/O or complex processing), +can produce noticeable GUI freezing or stuttering. When you don't see a way +to reduce algorithmic complexity, embed occasional `processEvents()` invocations +in heavy loops (see `Connection::saveState()` to get the idea). Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. Beware of premature optimization and have profiling data around before going into some hardcore optimization. Speaking of profiling logs (see README.md on how to turn them on) - in order to reduce small timespan logging spam, there's a default limit of at least -200 microseconds to log most operations with the PROFILER -(aka quotient.profile.debug) logging category. You can override this -limit by passing the new value (in microseconds) in PROFILER_LOG_USECS to -the compiler. In the future, this parameter will be made changeable at runtime -_if_ needed. +200 microseconds to log most operations with the `PROFILER` +(aka `quotient.profile.debug`) logging category. You can override this +limit by passing the new value (in microseconds) in `PROFILER_LOG_USECS` +definition to the compiler. If you happen to need to change it in runtime, +let me (`@kitsune`) know. ### Generated C++ code for CS API -The code in lib/csapi, lib/identity and lib/application-service, although -it resides in Git, is actually generated from (a soft fork of) the official -Matrix Swagger/OpenAPI definition files. Do not edit C++ files -in these directories by hand! - -Now, if you're unhappy with something in there and want to improve the code, -you have to understand the way these files are produced and setup +The code in `lib/csapi`, `lib/identity` and `lib/application-service`, although +it resides in Git, is actually generated from the official Swagger/OpenAPI +definition files. If you're unhappy with something in there and want to improve +the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling the below text can be found in .travis.yml (our Travis CI configuration actually regenerates those files upon every build). As described below, there is a handy build target for CMake; patches with a similar target for qmake are (you guessed it) very welcome. +BACKUP + You can edit C++ files in these directories by hand but be +aware that any `make update-api` invocation will overwrite your changes. +If you also happen to change the API definition files, you should place the new +revision of the generated C++ files in a separate commit. #### Why generate the code at all? -Because before both original authors of libQuotient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep, coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). +Because otherwise we have to do monkey business of writing boilerplate code, +with the same patterns, types etc., literally, for every single API endpoint, +and one of libQuotient authors got fed up with it at some point in time. +By then about 15 job classes were written; the entire API is about 100 endpoints +and counting. Besides, the existing jobs had to be updated according to changes +in CS API that have been, and will keep, coming. Other considerations can be +found in [this talk about API description languages](https://youtu.be/W5TmRozH-rg) +that also briefly touches on GTAD. #### Prerequisites for CS API code generation 1. Get the source code of GTAD and its dependencies, e.g. using the command: @@ -309,15 +343,16 @@ Because before both original authors of libQuotient had to do monkey business of 2. Build GTAD: in the source code directory, do `cmake . && cmake --build .` (you might need to pass `-DCMAKE_PREFIX_PATH=`, similar to libQuotient itself). -3. Get the Matrix CS API definitions that are included in the matrix-doc repo: - `git clone https://github.com/quotient-im/matrix-doc.git`. - The fork at `quotient-im/matrix-doc` is closely following the official - repo (`matrix-org/matrix-doc`), curating commits that are known to produce - working code for Quotient. You may want to use your own fork if you wish - to alter something in the API. +3. Get the Matrix CS API definitions that are included in a matrix-doc repo. + You can `git clone https://github.com/matrix-org/matrix-doc.git`, + the official repo; it's recommended though to instead + `git clone https://github.com/quotient-im/matrix-doc.git` - this repo closely + follows the official one, with an additional guarantee that you can always + generate working Quotient code from its HEAD commit. And of course you + can use your own repository if you need to change the API definition. 4. If you plan to submit a PR or just would like the generated code to be - formatted, you should either ensure you have clang-format (version 6 at least) - in your PATH or pass the _absolute_ path to it by adding + properly formatted, you should either ensure you have clang-format + (version 6 at least) in your PATH or pass the _absolute_ path to it by adding `-DCLANG_FORMAT=` to the CMake invocation below. #### Generating CS API contents @@ -332,24 +367,29 @@ Because before both original authors of libQuotient had to do monkey business of files in `lib/csapi`, `lib/identity`, `lib/application-service` for all YAML files it can find in `matrix-doc/api/client-server` and other files in `matrix-doc/api` these depend on. -3. Re-run CMake so that the build system knows about new files, if there are any. +3. Re-run CMake so that the build system knows about new files, if there are any + (this step is unnecessary if you use CMake 3.12 or later). #### Changing generated code See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQuotient are described here. GTAD uses the following three kinds of sources: -1. OpenAPI files. Each file is treated as a separate source (because this is how GTAD works now). -2. A configuration file, in our case it's lib/csapi/gtad.yaml - this one is common for the whole API. -3. Source code template files: lib/csapi/{{base}}.*.mustache - also common. - -The mustache files have a templated (not in C++ sense) definition of a network -job, deriving from BaseJob; each job class is prepended, if necessary, with -data structure definitions used by this job. The look of those files is hideous -for a newcomer; and the only known highlighter that can handle the combination -of Mustache (originally a web templating language) and C++ is provided in CLion. -To slightly simplify things some more or less generic constructs are defined -in gtad.yaml (see its "mustache:" section). Adventurous souls that would like -to figure what's going on in these files should speak up in the Quotient room - +1. OpenAPI files. Each file is treated as a separate source (if you worked with + swagger-codegen - you do _not_ need to have a single file for the whole API). +2. A configuration file, in our case it's `gtad/gtad.yaml` - this one is common + for all OpenAPI files GTAD is invoked on. +3. Source code template files: `gtad/*.mustache` - are also common. + +The Mustache files have a templated (not in C++ sense) definition of a network +job, deriving from BaseJob; if necessary, data structure definitions used +by this job are put before the job class. Bigger Mustache files look a bit +hideous for a newcomer; and the only known highlighter that can handle +the combination of Mustache (originally a web templating language) and C++ is +provided in CLion IDE. Fortunately, all our Mustache files are reasonably +concise and well-formatted these days. +To simplify things some reusable Mustache blocks are defined in `gtad.yaml` - +see its `mustache:` section. Adventurous souls that would like to figure +what's going on in these files should speak up in the Quotient room - I (Kitsune) will be very glad to help you out. The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": @@ -400,6 +440,39 @@ of a Qt container, or unguarded null pointers. You can use this time to time (see Analyze menu in Qt Creator) instead of hogging your machine with deep analysis as you type. +### Submitting API changes + +If you changed the API definitions, the path to upstream becomes somewhat +intricate, as you have to coordinate with two projects, making up to 4 PRs along +the way. The recommended sequence depends on whether or not you have to +[write an Matrix Spec Change aka MSC](https://matrix.org/docs/spec/proposals). +Usually you have to, unless your API changes keep API semantics intact. +In that case: +1. Submit an MSC before submitting changes to the API definition files and + libQuotient. +2. The MSC gets reviewed by the Spec Core Team. This can be a lengthy process + but it's necessary for the Matrix ecosystem integrity. +3. When your MSC has at least some approvals (not necessarily a complete + acceptance but at least some approvals should be there) submit a PR to + libQuotient, referring to your `matrix-doc` repo. Make sure that generated + files are committed separately from non-generated ones (no need to make two + PRs; just separate them in different commits). +4. If your libQuotient PR is approved and MSC is not there yet you'll be asked + to submit a PR with API definition files at + `https://github.com/quotient-im/matrix-doc`. Note that this is _not_ + an official repo; but you can refer to your libQuotient PR as + an _implementation_ of the MSC - a necessary step before making a so-called + "spec PR". +5. Once MSC is accepted, submit your `matrix-doc` changes as a PR to + `https://github.com/matrix-org/matrix-doc` (the "spec PR" mentioned above). + This will require that your submission meets the standards set by this + project (they are quite reasonable and not too hard to meet). + +If your changes don't need an MSC, it becomes a more straightforward combination +of 2 PRs: one to `https://github.com/matrix-org/matrix-doc` ("spec PR") and one +to libQuotient (with the same guidance about putting generated and non-generated +files in different commits). + ## Git commit messages When writing git commit messages, try to follow the guidelines in diff --git a/README.md b/README.md index 7dbb5dbc..0ed599b3 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,18 @@ Just install things from the list above using your preferred package manager. If `brew install qt5` should get you a recent Qt5. If you plan to use CMake, you will need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` #### Windows -1. Install Qt5, using their official installer. -1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for macOS (see above), also helps. +Install Qt5, using their official installer; if you plan to build with CMake, +make sure to tick the CMake box in the list of installed components. + +The commands in further sections imply that cmake/qmake is in your PATH, +otherwise you have to prepend those commands with actual paths. As an option +it's a good idea to run a `qtenv2.bat` script that can be found in +`C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`); +the only thing it does is adding necessary paths to PATH. You might not want +to run that script on system startup but it's very handy to setup +the environment before building. For CMake you can alternatively point +`CMAKE_PREFIX_PATH` to your Qt installation and leave PATH unchanged; but +in that case you'll have to supply the full path to CMake when calling it. ### Using the library If you use CMake, `find_package(Quotient)` sets up the client code to use @@ -70,14 +80,13 @@ introducing a submodule in your source tree and build it along with the rest of the application for now. Note also that qmake is considered for phase-out in Qt 6 so you should probably think of moving over to CMake eventually. -Building with dynamic linkage are only tested on Linux at the moment and are +Building with dynamic linkage is only tested on Linux at the moment and is a recommended way of linking your application with libQuotient on this platform. -Feel free Static linkage is the default on Windows/macOS; feel free to experiment with dynamic linking and submit PRs if you get reusable results. -The example/test application that comes with libQuotient, -[quotest](tests) includes most common use cases such as sending messages, uploading files, +[Quotest](tests), the test application that comes with libQuotient, includes +most common use cases such as sending messages, uploading files, setting room state etc.; for more extensive usage check out the source code of [Quaternion](https://github.com/quotient-im/Quaternion) (the reference client of Quotient) or [Spectral](https://gitlab.com/b0/spectral). diff --git a/SECURITY.md b/SECURITY.md index 28f19f4b..e821aed1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,9 +11,9 @@ ## Reporting a Vulnerability -If you find a significant vulnerability, or evidence of one, use either of the following contacts: -- send an email to [Kitsune Ral](mailto:Kitsune-Ral@users.sf.net); or -- reach out in Matrix to [@kitsune:matrix.org](https://matrix.to/#/@kitsune:matrix.org) (if you can, switch encryption on). +If you find a vulnerability, or evidence of one, use either of the following contacts: +- via email: [Kitsune Ral](mailto:Kitsune-Ral@users.sf.net); or +- via Matrix: [direct chat with @kitsune:matrix.org](https://matrix.to/#/@kitsune:matrix.org?action=chat). In any of these two options, indicate that you have such information (do not share it yet), and we'll tell you the next steps. @@ -24,10 +24,14 @@ so this is NOT the right way for undisclosed vulnerabilities, whether or not you ## Timeline and commitments -Initial reaction to the message about a vulnerability (see above) will be no more than 5 days. From the moment of the private report or -public disclosure (if it hasn't been reported earlier in private) of each vulnerability, we take effort to fix it on priority before -any other issues. In case of vulnerabilities with [CVSS v2](https://nvd.nist.gov/cvss.cfm) score of 4.0 and higher the commitment is -to provide a workaround within 30 days and a full fix within 60 days after the specific information on the vulnerability has been -reported to the project by any means (in private or in public). For vulnerabilities with lower score there is no commitment on the timeline, -only prioritisation. The full fix doesn't imply that all software functionality remains accessible (in the worst case +Initial reaction to the message about a vulnerability (see above) will be +no more than 5 days. From the moment of the private report or public disclosure +(if it hasn't been reported earlier in private) of each vulnerability, we take +effort to fix it on priority before any other issues. In case of vulnerabilities +with [CVSS v2](https://nvd.nist.gov/cvss.cfm) score of 4.0 and higher +the commitment is to provide a workaround within 30 days and a full fix +within 60 days after the project has been made aware about the vulnerability +(in private or in public). For vulnerabilities with lower score there is +no commitment on the timeline, only prioritisation. The full fix doesn't imply +that all software functionality remains accessible (in the worst case the vulnerable functionality may be disabled or removed to prevent the attack). -- 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 ++++---- lib/room.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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(); diff --git a/lib/room.h b/lib/room.h index 7fc03c19..4be3ed2b 100644 --- a/lib/room.h +++ b/lib/room.h @@ -555,7 +555,7 @@ public slots: /// You shouldn't normally call this method; it's here for debugging void refreshDisplayName(); - void getPreviousContent(int limit = 10); + void getPreviousContent(int limit = 10, const QString &filter = {}); void inviteToRoom(const QString& memberId); LeaveRoomJob* leaveRoom(); -- cgit v1.2.3 From e00b3674f9fd8b606ea4d293dfb078ed4a621e12 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 11 Sep 2020 06:50:45 +0200 Subject: More stringent serverpart checks in user ids May lead to new crashes due to nullptr returned from Connection::user() on more utterly invalid content from the wire that the library still doesn't properly invalidate. This has long been quite a good case for exceptions, or another error-handling framework: Connection::user() can return nullptr either when out of memory or when the id is invalid or empty, and other places are likely to treat invalid ids in different ways but probably just hope that memory exhaustion "never happens", or try to handle it in a quite different way than an empty or invalid id. Something to think of in 0.7. (cherry picked from commit 3c85f049389dec3b0ee6406f0be2cfaf0089f1fe) --- lib/connection.cpp | 2 +- lib/util.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d3324d47..0f53643b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1151,7 +1151,7 @@ User* Connection::user(const QString& uId) { if (uId.isEmpty()) return nullptr; - if (!uId.startsWith('@') || !uId.contains(':')) { + if (!uId.startsWith('@') || serverPart(uId).isEmpty()) { qCCritical(MAIN) << "Malformed userId:" << uId; return nullptr; } diff --git a/lib/util.cpp b/lib/util.cpp index 797f6d69..023645c1 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -116,13 +116,13 @@ qreal Quotient::stringToHueF(const QString& s) } static const auto ServerPartRegEx = QStringLiteral( - "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address + "(\\[[^][:blank:]]+\\]|[-[:alnum:].]+)" // Either IPv6 address or hostname/IPv4 address "(?::(\\d{1,5}))?" // Optional port ); QString Quotient::serverPart(const QString& mxId) { - static QString re = "^[@!#$+].+?:(" // Localpart and colon + static QString re = "^[@!#$+].*?:(" // Localpart and colon % ServerPartRegEx % ")$"; static QRegularExpression parser( re, -- cgit v1.2.3 From 2753ebd6c6c320e459d2cab65e8a0328138c1d60 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 11 Sep 2020 07:10:32 +0200 Subject: util.cpp: assert validity of regular expressions (cherry picked from commit 0e87640560343c15b0a218796509d2d94e1a5c77) --- lib/util.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/util.cpp b/lib/util.cpp index 023645c1..ffb36068 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -60,6 +60,8 @@ void Quotient::linkifyUrls(QString& htmlEscapedText) QStringLiteral( R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"), RegExpOptions); + Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid() + && MxIdRegExp.isValid()); // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," @@ -127,6 +129,7 @@ QString Quotient::serverPart(const QString& mxId) static QRegularExpression parser( re, QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits + Q_ASSERT(parser.isValid()); return parser.match(mxId).captured(1); } -- cgit v1.2.3 From c5b162574d937408927d0ad89fb0c6ba96d49dd8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 11 Sep 2020 07:15:01 +0200 Subject: Uri::toUrl(): fix incorrect matrix.to rep The first character inside the fragment should be / (cherry picked from commit 948be2ef2bf04e306fbb0e2c3e0a98f4151337a7) --- lib/uri.cpp | 2 +- tests/quotest.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/uri.cpp b/lib/uri.cpp index f813794c..8370d83a 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -134,7 +134,7 @@ QUrl Uri::toUrl(UriForm form) const url.setScheme("https"); url.setHost("matrix.to"); url.setPath("/"); - auto fragment = primaryId(); + auto fragment = '/' + primaryId(); if (const auto& secId = secondaryId(); !secId.isEmpty()) fragment += '/' + secId; if (const auto& q = query(); !q.isEmpty()) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 3a32da69..3b7f2f48 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -649,6 +649,11 @@ TEST_IMPL(visitResources) Uri uri { uriString }; clog << "Checking " << uriString.toStdString() << " -> " << uri.toDisplayString().toStdString() << endl; + if (auto matrixToUrl = uri.toUrl(Uri::MatrixToUri).toDisplayString(); + !matrixToUrl.startsWith("https://matrix.to/#/")) { + clog << "Incorrect matrix.to representation:" + << matrixToUrl.toStdString() << endl; + } ud.visitResource(connection(), uriString); if (spy.count() != 1) { clog << "Wrong number of signal emissions (" << spy.count() -- cgit v1.2.3 From 5f2bc54070b3b56070e308f3336c2113dcb62122 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 6 Nov 2020 14:08:25 +0100 Subject: .clang-format: add SpaceInEmptyBlock: false (clang 10) --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index 5d1f6485..9ae1dc05 100644 --- a/.clang-format +++ b/.clang-format @@ -116,6 +116,7 @@ SortUsingDeclarations: false #SpaceBeforeInheritanceColon: true #SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyBlock: false #SpaceInEmptyParentheses: false #SpacesBeforeTrailingComments: 1 #SpacesInAngles: false -- cgit v1.2.3 From 91cb9913f94c105b26eb4cfb99562eb86ba0f16a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 6 Nov 2020 14:09:23 +0100 Subject: Further restrict IPv6 branch of ServerPartRegEx --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index ffb36068..0c1c54ff 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -118,7 +118,7 @@ qreal Quotient::stringToHueF(const QString& s) } static const auto ServerPartRegEx = QStringLiteral( - "(\\[[^][:blank:]]+\\]|[-[:alnum:].]+)" // Either IPv6 address or hostname/IPv4 address + "(\\[[^][:space:]]+]|[-[:alnum:].]+)" // IPv6 address or hostname/IPv4 address "(?::(\\d{1,5}))?" // Optional port ); -- cgit v1.2.3 From 21fd088d207ee2c2724c5b250875a843b618497d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 6 Nov 2020 14:11:49 +0100 Subject: ChangePasswordJob: logoutDevices doesn't need Omittable This is generated by GTAD 0.7.1 (to be released), based on the changed registration.yaml (to be committed and pulled). --- lib/csapi/registration.cpp | 2 +- lib/csapi/registration.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index b80abc84..33f61265 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -58,7 +58,7 @@ RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( } ChangePasswordJob::ChangePasswordJob(const QString& newPassword, - Omittable logoutDevices, + bool logoutDevices, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), QStringLiteral("/_matrix/client/r0") % "/account/password") diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 62bc35f1..6864fd47 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -222,7 +222,7 @@ public: * * \param logoutDevices * Whether the user's other access tokens, and their associated devices, - * should be revoked if the request succeeds. Defaults to true. + * should be revoked if the request succeeds. * * When ``false``, the server can still take advantage of `the soft logout * method <#soft-logout>`_ for the user's remaining devices. @@ -232,7 +232,7 @@ public: * authentication API. */ explicit ChangePasswordJob(const QString& newPassword, - Omittable logoutDevices = none, + bool logoutDevices = true, const Omittable& auth = none); }; -- cgit v1.2.3 From 7355e2f801eb85e771a6c454c77f9eb62398f38b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 8 Nov 2020 18:53:37 +0100 Subject: converters.h: add QUrl support --- lib/converters.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/converters.h b/lib/converters.h index 543e9496..81d7b6d8 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -165,6 +165,14 @@ struct JsonConverter { } }; +template <> +struct JsonConverter : JsonConverter { + static auto dump(const QUrl& url) // Override on top of that for QString + { + return JsonConverter::dump(url.toString(QUrl::FullyEncoded)); + } +}; + template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toArray(); } -- 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/events/roommemberevent.cpp | 28 +++++++++----- lib/events/roommemberevent.h | 18 ++++++--- lib/room.cpp | 85 +++++++++++++++++++++++++++++++++--------- lib/room.h | 47 ++++++++++++++++++----- lib/user.cpp | 10 ++--- 5 files changed, 139 insertions(+), 49 deletions(-) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 35cbdb3a..f6b29f7f 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -50,10 +50,13 @@ using namespace Quotient; MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) - , displayName(sanitized(json["displayname"_ls].toString())) - , avatarUrl(json["avatar_url"_ls].toString()) + , displayName(fromJson>(json["displayname"_ls])) + , avatarUrl(fromJson>(json["avatar_url"_ls])) , reason(json["reason"_ls].toString()) -{} +{ + if (displayName) + displayName = sanitized(*displayName); +} void MemberEventContent::fillJson(QJsonObject* o) const { @@ -62,9 +65,10 @@ void MemberEventContent::fillJson(QJsonObject* o) const "The key 'membership' must be explicit in MemberEventContent"); if (membership != MembershipType::Undefined) o->insert(QStringLiteral("membership"), membershipStrings[membership]); - o->insert(QStringLiteral("displayname"), displayName); - if (avatarUrl.isValid()) - o->insert(QStringLiteral("avatar_url"), avatarUrl.toString()); + if (displayName) + o->insert(QStringLiteral("displayname"), *displayName); + if (avatarUrl && avatarUrl->isValid()) + o->insert(QStringLiteral("avatar_url"), avatarUrl->toString()); if (!reason.isEmpty()) o->insert(QStringLiteral("reason"), reason); } @@ -111,12 +115,16 @@ bool RoomMemberEvent::isUnban() const bool RoomMemberEvent::isRename() const { - auto prevName = prevContent() ? prevContent()->displayName : QString(); - return displayName() != prevName; + auto prevName = prevContent() && prevContent()->displayName + ? *prevContent()->displayName + : QString(); + return newDisplayName() != prevName; } bool RoomMemberEvent::isAvatarUpdate() const { - auto prevAvatarUrl = prevContent() ? prevContent()->avatarUrl : QUrl(); - return avatarUrl() != prevAvatarUrl; + auto prevAvatarUrl = prevContent() && prevContent()->avatarUrl + ? *prevContent()->avatarUrl + : QUrl(); + return newAvatarUrl() != prevAvatarUrl; } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 783b8207..35fd69a9 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -24,7 +24,7 @@ namespace Quotient { class MemberEventContent : public EventContent::Base { public: - enum MembershipType : size_t { + enum MembershipType : unsigned char { Invite = 0, Join, Knock, @@ -38,8 +38,8 @@ public: MembershipType membership; bool isDirect = false; - QString displayName; - QUrl avatarUrl; + Omittable displayName; + Omittable avatarUrl; QString reason; protected: @@ -84,8 +84,16 @@ public: MembershipType membership() const { return content().membership; } QString userId() const { return fullJson()[StateKeyKeyL].toString(); } bool isDirect() const { return content().isDirect; } - QString displayName() const { return content().displayName; } - QUrl avatarUrl() const { return content().avatarUrl; } + Omittable newDisplayName() const { return content().displayName; } + Omittable newAvatarUrl() const { return content().avatarUrl; } + [[deprecated("Use newDisplayName() instead")]] QString displayName() const + { + return newDisplayName().value_or(QString()); + } + [[deprecated("Use newAvatarUrl() instead")]] QUrl avatarUrl() const + { + return newAvatarUrl().value_or(QUrl()); + } QString reason() const { return content().reason; } bool changesMembership() const; bool isBan() const; 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); diff --git a/lib/room.h b/lib/room.h index 4be3ed2b..d7b5c516 100644 --- a/lib/room.h +++ b/lib/room.h @@ -101,7 +101,7 @@ class Room : public QObject { Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) - Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) + Q_PROPERTY(QStringList memberNames READ safeMemberNames NOTIFY memberListChanged) Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) @@ -209,7 +209,10 @@ public: QList membersLeft() const; Q_INVOKABLE QList users() const; + [[deprecated("Use safeMemberNames() or htmlSafeMemberNames() instead")]] QStringList memberNames() const; + QStringList safeMemberNames() const; + QStringList htmlSafeMemberNames() const; [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] int memberCount() const; int timelineSize() const; @@ -251,31 +254,55 @@ public: /** * \brief Check the join state of a given user in this room * - * \note Banned and invited users are not tracked for now (Leave + * \note Banned and invited users are not tracked separately for now (Leave * will be returned for them). * * \return Join if the user is a room member; Leave otherwise */ Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; - /** - * Get a disambiguated name for a given user in - * the context of the room + //! \brief Get a display name (without disambiguation) for the given member + //! + //! \sa safeMemberName, htmlSafeMemberName + Q_INVOKABLE QString memberName(const QString& mxId) const; + + /*! + * \brief Get a disambiguated name for the given user in the room context + * + * \deprecated use safeMemberName() instead */ Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; - /** - * Get a disambiguated name for a user with this id in - * the context of the room + /*! + * \brief Get a disambiguated name for a user with this id in the room context + * + * \deprecated use safeMemberName() instead */ Q_INVOKABLE QString roomMembername(const QString& userId) const; - /** Get a display-safe member name in the context of this room + /*! + * \brief Get a disambiguated name for the member with the given MXID * - * Display-safe means HTML-safe + without RLO/LRO markers + * This function should only be used for non-UI code; consider using + * safeMemberName() or htmlSafeMemberName() for displayed strings. + */ + Q_INVOKABLE QString disambiguatedMemberName(const QString& mxId) const; + + /*! Get a display-safe member name in the context of this room + * + * Display-safe means disambiguated and without RLO/LRO markers * (see https://github.com/quotient-im/Quaternion/issues/545). */ Q_INVOKABLE QString safeMemberName(const QString& userId) const; + /*! Get an HTML-safe member name in the context of this room + * + * This function adds HTML escaping on top of safeMemberName() safeguards. + */ + Q_INVOKABLE QString htmlSafeMemberName(const QString& userId) const; + + //! \brief Get an avatar for the member with the given MXID + QUrl memberAvatarUrl(const QString& mxId) const; + const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; diff --git a/lib/user.cpp b/lib/user.cpp index ffa4efb9..45a9c121 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -88,8 +88,7 @@ int User::hue() const { return int(hueF() * 359); } QString User::name(const Room* room) const { - return room ? room->getCurrentState(id())->displayName() - : d->defaultName; + return room ? room->memberName(id()) : d->defaultName; } QString User::rawName(const Room* room) const { return name(room); } @@ -169,9 +168,8 @@ bool User::isIgnored() const { return connection()->isIgnored(this); } QString User::displayname(const Room* room) const { - return room ? room->roomMembername(this) - : d->defaultName.isEmpty() ? d->id - : d->defaultName; + return room ? room->safeMemberName(id()) + : d->defaultName.isEmpty() ? d->id : d->defaultName; } QString User::fullName(const Room* room) const @@ -187,7 +185,7 @@ const Avatar& User::avatarObject(const Room* room) const if (!room) return d->defaultAvatar; - const auto& url = room->getCurrentState(id())->avatarUrl(); + const auto& url = room->memberAvatarUrl(id()); const auto& mediaId = url.authority() + url.path(); return d->otherAvatars.try_emplace(mediaId, url).first->second; } -- 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 ------ lib/room.h | 3 --- 2 files changed, 9 deletions(-) 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); diff --git a/lib/room.h b/lib/room.h index d7b5c516..f4d7eb70 100644 --- a/lib/room.h +++ b/lib/room.h @@ -586,9 +586,6 @@ public slots: void inviteToRoom(const QString& memberId); LeaveRoomJob* leaveRoom(); - /// \deprecated - use setState() instead") - SetRoomStateWithKeyJob* setMemberState(const QString& memberId, - const RoomMemberEvent& event) const; void kickMember(const QString& memberId, const QString& reason = {}); void ban(const QString& userId, const QString& reason = {}); void unban(const QString& userId); -- cgit v1.2.3 From b27ef93df9b5147ab03c8a255918874b9f73c201 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 10 Nov 2020 13:07:28 +0100 Subject: MembershipType: drop warning on empty values This is a usual situation when a membership type is undefined; and the current code constructs _a lot_ of stub events by loading them from empty JSON. So just silence those warnings for now. --- lib/events/roommemberevent.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index f6b29f7f..be47e412 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -39,7 +39,8 @@ struct JsonConverter { if (membershipString == *it) return MembershipType(it - membershipStrings.begin()); - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; + if (!membershipString.isEmpty()) + qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; return MembershipType::Undefined; } }; -- cgit v1.2.3 From 39b80fa8eb9b70d287aa453663b845319d8b6196 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 11 Nov 2020 19:17:23 +0100 Subject: .clang-format: update for ClangFormat 10+ Also: add space before colon in range-based for from now on. [skip ci] --- .clang-format | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/.clang-format b/.clang-format index 9ae1dc05..7cc0f46e 100644 --- a/.clang-format +++ b/.clang-format @@ -19,23 +19,29 @@ Language: Cpp BasedOnStyle: WebKit #AccessModifierOffset: -4 AlignAfterOpenBracket: Align +#AlignConsecutiveMacros: false #AlignConsecutiveAssignments: false #AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left AlignOperands: true #AlignTrailingComments: false +#AllowAllArgumentsOnNextLine: true +#AllowAllConstructorInitializersOnNextLine: true #AllowAllParametersOfDeclarationOnNextLine: true -#AllowShortBlocksOnASingleLine: false +#AllowShortBlocksOnASingleLine: false # 'Empty' since ClangFormat 10 #AllowShortCaseLabelsOnASingleLine: false #AllowShortFunctionsOnASingleLine: All -#AllowShortIfStatementsOnASingleLine: false +#AllowShortLambdasOnASingleLine: All +#AllowShortIfStatementsOnASingleLine: false # 'Never' since ClangFormat 10 #AllowShortLoopsOnASingleLine: false +#AlwaysBreakAfterDefinitionReturnType: None # deprecated #AlwaysBreakAfterReturnType: None #AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: true +AlwaysBreakTemplateDeclarations: Yes #BinPackArguments: true #BinPackParameters: true BraceWrapping: + AfterCaseLabel: false AfterClass: false AfterControlStatement: false AfterEnum: false @@ -53,21 +59,21 @@ BraceWrapping: BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom #BreakBeforeInheritanceComma: false -#BreakInheritanceList: BeforeColon # Only supported since clang-format 7 +#BreakInheritanceList: BeforeColon #BreakBeforeTernaryOperators: true #BreakConstructorInitializersBeforeComma: false #BreakConstructorInitializers: BeforeComma #BreakStringLiterals: true ColumnLimit: 80 -#CommentPragmas: '^!|^:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true #ConstructorInitializerIndentWidth: 4 #ContinuationIndentWidth: 4 Cpp11BracedListStyle: false +#DeriveLineEnding: true #DerivePointerAlignment: false FixNamespaceComments: true -ForEachMacros: +ForEachMacros: - foreach - Q_FOREACH - forever @@ -83,14 +89,13 @@ IncludeCategories: Priority: 4 - Regex: '.*' Priority: 1 -IncludeIsMainRegex: '(_test)?$' +#IncludeIsMainRegex: '(_test)?$' +#IncludeIsMainSourceRegex: '' #IndentCaseLabels: false +IndentGotoLabels: false IndentPPDirectives: AfterHash -#IndentWidth: 4 +#IndentWidth: 4 #IndentWrappedFunctionNames: false -#JavaScriptQuotes: Leave -#JavaScriptWrapImports: true -#KeepLineBreaksForNonEmptyLines: false KeepEmptyLinesAtTheStartOfBlocks: false #MacroBlockBegin: '' #MacroBlockEnd: '' @@ -105,26 +110,34 @@ PenaltyBreakString: 200 PenaltyExcessCharacter: 20 PenaltyReturnTypeOnItsOwnLine: 60 #PointerAlignment: Left -#ReflowComments: true +#ReflowComments: true #SortIncludes: true SortUsingDeclarations: false #SpaceAfterCStyleCast: false +#SpaceAfterLogicalNot: false #SpaceAfterTemplateKeyword: true #SpaceBeforeAssignmentOperators: true #SpaceBeforeCpp11BracedList: true #SpaceBeforeCtorInitializerColon: true #SpaceBeforeInheritanceColon: true #SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: false +SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyBlock: false #SpaceInEmptyParentheses: false #SpacesBeforeTrailingComments: 1 #SpacesInAngles: false +#SpacesInConditionalStatement: false #SpacesInContainerLiterals: true #SpacesInCStyleCastParentheses: false #SpacesInParentheses: false #SpacesInSquareBrackets: false -Standard: Cpp11 -#TabWidth: 4 +#SpaceBeforeSquareBrackets: false +Standard: Cpp11 # Once on ClangFormat 10, switch to Cpp17 +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +#UseCRLF: false #UseTab: Never ... + -- 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/connection.cpp | 2 +- lib/events/event.h | 4 ++++ lib/events/roomevent.cpp | 8 ++++---- lib/events/roomkeyevent.h | 2 +- lib/room.cpp | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0f53643b..e84b8080 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -202,7 +202,7 @@ public: auto&& decryptedEvent = fromJson(QJsonDocument::fromJson(decrypted.toUtf8())); - if (auto sender = decryptedEvent->fullJson()["sender"_ls].toString(); + if (auto sender = decryptedEvent->fullJson()[SenderKeyL].toString(); sender != encryptedEvent.senderId()) { qCWarning(E2EE) << "Found user" << sender << "instead of sender" << encryptedEvent.senderId() diff --git a/lib/events/event.h b/lib/events/event.h index 5b9f20b7..b12dc9ad 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -59,12 +59,16 @@ static const auto TypeKey = QStringLiteral("type"); static const auto BodyKey = QStringLiteral("body"); static const auto ContentKey = QStringLiteral("content"); static const auto EventIdKey = QStringLiteral("event_id"); +static const auto SenderKey = QStringLiteral("sender"); +static const auto RoomIdKey = QStringLiteral("room_id"); static const auto UnsignedKey = QStringLiteral("unsigned"); static const auto StateKeyKey = QStringLiteral("state_key"); static const auto TypeKeyL = "type"_ls; static const auto BodyKeyL = "body"_ls; static const auto ContentKeyL = "content"_ls; static const auto EventIdKeyL = "event_id"_ls; +static const auto SenderKeyL = "sender"_ls; +static const auto RoomIdKeyL = "room_id"_ls; static const auto UnsignedKeyL = "unsigned"_ls; static const auto RedactedCauseKeyL = "redacted_because"_ls; static const auto PrevContentKeyL = "prev_content"_ls; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 0a4332ad..3d87ef18 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -51,12 +51,12 @@ QDateTime RoomEvent::originTimestamp() const QString RoomEvent::roomId() const { - return fullJson()["room_id"_ls].toString(); + return fullJson()[RoomIdKeyL].toString(); } QString RoomEvent::senderId() const { - return fullJson()["sender"_ls].toString(); + return fullJson()[SenderKeyL].toString(); } bool RoomEvent::isReplaced() const @@ -90,12 +90,12 @@ QString RoomEvent::stateKey() const void RoomEvent::setRoomId(const QString& roomId) { - editJson().insert(QStringLiteral("room_id"), roomId); + editJson().insert(RoomIdKey, roomId); } void RoomEvent::setSender(const QString& senderId) { - editJson().insert(QStringLiteral("sender"), senderId); + editJson().insert(SenderKey, senderId); } void RoomEvent::setTransactionId(const QString& txnId) diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 679cbf7c..3a781474 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -11,7 +11,7 @@ public: RoomKeyEvent(const QJsonObject& obj); QString algorithm() const { return content("algorithm"_ls); } - QString roomId() const { return content("room_id"_ls); } + QString roomId() const { return content(RoomIdKeyL); } QString sessionId() const { return content("session_id"_ls); } QString sessionKey() const { return content("session_key"_ls); } }; 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(-) 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 da4bc5afcfeb9e7fa1253720839291194578bd4a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 14 Nov 2020 19:58:58 +0100 Subject: ConnectionData: fix defunct jobs stalling the queue --- lib/connectiondata.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index e806f952..d57363d0 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -67,7 +67,8 @@ ConnectionData::ConnectionData(QUrl baseUrl) d->rateLimiter.setInterval(0); for (auto& q : d->jobs) while (!q.empty()) { - auto& job = q.front(); + const auto job = q.front(); + q.pop(); if (!job || job->error() == BaseJob::Abandoned) continue; if (job->error() != BaseJob::Pending) { @@ -79,7 +80,6 @@ ConnectionData::ConnectionData(QUrl baseUrl) } job->sendRequest(); d->rateLimiter.start(); - q.pop(); return; } qCDebug(MAIN) << d->id() << "job queues are empty"; -- cgit v1.2.3 From 8f4c7f67930be402836ca7a6266ba4277266a12d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 14 Nov 2020 20:01:36 +0100 Subject: Drop EventFactory RoomPowerLevelsEvent is not used in csapi/ classes so the factory is of no use either. --- lib/events/roompowerlevelsevent.h | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index f0f7207f..b832230e 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -62,15 +62,5 @@ public: private: }; -template <> -class EventFactory { -public: - static event_ptr_tt make(const QJsonObject& json, - const QString&) - { - return makeEvent(json); - } -}; - REGISTER_EVENT_TYPE(RoomPowerLevelsEvent) } // namespace Quotient -- 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/events/roomcreateevent.cpp | 4 ++-- lib/events/stateevent.h | 5 ++--- lib/room.cpp | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index c72b5bc2..0fc7d6b9 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -33,8 +33,8 @@ QString RoomCreateEvent::version() const RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const { const auto predJson = contentJson()["predecessor"_ls].toObject(); - return { fromJson(predJson["room_id"_ls]), - fromJson(predJson["event_id"_ls]) }; + return { fromJson(predJson[RoomIdKeyL]), + fromJson(predJson[EventIdKeyL]) }; } bool RoomCreateEvent::isUpgrade() const diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 710b4271..400858db 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -23,12 +23,11 @@ namespace Quotient { /// Make a minimal correct Matrix state event JSON -template -inline QJsonObject basicStateEventJson(StrT matrixType, +inline QJsonObject basicStateEventJson(const QString& matrixTypeId, const QJsonObject& content, const QString& stateKey = {}) { - return { { TypeKey, std::forward(matrixType) }, + return { { TypeKey, matrixTypeId }, { StateKeyKey, stateKey }, { ContentKey, content } }; } 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 5b1bfc102fccd4e57893b34bf2b0a14ba6a9f577 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 14 Nov 2020 22:35:31 +0100 Subject: Make StateEventBase Q_GADGET too To align with the two other base event classes (Event and RoomEvent). --- lib/events/stateevent.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 400858db..20a85f83 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -33,6 +33,8 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, } class StateEventBase : public RoomEvent { + Q_GADGET + Q_PROPERTY(QString stateKey READ stateKey CONSTANT) public: using factory_t = EventFactory; @@ -128,3 +130,5 @@ private: std::unique_ptr> _prev; }; } // namespace Quotient +Q_DECLARE_METATYPE(Quotient::StateEventBase*) +Q_DECLARE_METATYPE(const Quotient::StateEventBase*) -- 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/events/callanswerevent.cpp | 5 ----- lib/events/callhangupevent.cpp | 5 ----- lib/events/callinviteevent.cpp | 5 ----- lib/events/encryptedevent.cpp | 3 --- lib/events/encryptionevent.h | 4 +--- lib/events/event.h | 12 ------------ lib/events/reactionevent.h | 3 --- lib/events/receiptevent.cpp | 12 +++++++----- lib/events/receiptevent.h | 10 ++-------- lib/events/roomcanonicalaliasevent.h | 4 ++-- lib/events/roomevent.cpp | 5 ++--- lib/events/roomkeyevent.h | 2 +- lib/events/roommemberevent.h | 10 ++-------- lib/events/typingevent.cpp | 8 ++------ lib/events/typingevent.h | 7 ++----- lib/room.cpp | 11 ++++++----- 16 files changed, 27 insertions(+), 79 deletions(-) diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index d6622b30..bf096534 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -18,11 +18,6 @@ #include "callanswerevent.h" -#include "event.h" -#include "logging.h" - -#include - /* m.call.answer { diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp index d41849c3..f2117f38 100644 --- a/lib/events/callhangupevent.cpp +++ b/lib/events/callhangupevent.cpp @@ -18,11 +18,6 @@ #include "callhangupevent.h" -#include "event.h" -#include "logging.h" - -#include - /* m.call.hangup { diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 54faac8d..63f331de 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -18,11 +18,6 @@ #include "callinviteevent.h" -#include "event.h" -#include "logging.h" - -#include - /* m.call.invite { diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index dccfa540..117aae37 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -1,9 +1,6 @@ #include "encryptedevent.h" -#include "room.h" - using namespace Quotient; -using namespace QtOlm; EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext, const QString& senderKey) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index cbd3ba4a..cbb6d786 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -48,6 +48,7 @@ public: DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) using EncryptionType = EncryptionEventContent::EncryptionType; + Q_ENUM(EncryptionType) explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate // default value @@ -64,9 +65,6 @@ public: QString algorithm() const { return content().algorithm; } int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } - -private: - Q_ENUM(EncryptionType) }; REGISTER_EVENT_TYPE(EncryptionEvent) diff --git a/lib/events/event.h b/lib/events/event.h index b12dc9ad..626a0229 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -21,10 +21,6 @@ #include "converters.h" #include "logging.h" -#ifdef ENABLE_EVENTTYPE_ALIAS -# define USE_EVENTTYPE_ALIAS 1 -#endif - namespace Quotient { // === event_ptr_tt<> and type casting facilities === @@ -45,14 +41,6 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt& ptr) return static_cast(rawPtr(ptr)); } -/// Re-wrap a smart pointer to base into a smart pointer to derived -template -[[deprecated("Consider using eventCast() or visit() instead")]] -inline event_ptr_tt ptrCast(event_ptr_tt&& ptr) -{ - return std::unique_ptr(static_cast(ptr.release())); -} - // === Standard Matrix key names and basicEventJson() === static const auto TypeKey = QStringLiteral("type"); diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 75c6528c..48b0bc6c 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -64,9 +64,6 @@ public: { return content(QStringLiteral("m.relates_to")); } - -private: - EventRelation _relation; }; REGISTER_EVENT_TYPE(ReactionEvent) diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index bf050cb2..deb3c4e8 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -40,10 +40,11 @@ Example of a Receipt Event: using namespace Quotient; -ReceiptEvent::ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) +EventsWithReceipts ReceiptEvent::eventsWithReceipts() const { + EventsWithReceipts result; const auto& contents = contentJson(); - _eventsWithReceipts.reserve(contents.size()); + result.reserve(contents.size()); for (auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt) { if (eventIt.key().isEmpty()) { qCWarning(EPHEMERAL) @@ -51,15 +52,16 @@ ReceiptEvent::ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; continue; } - const QJsonObject reads = + const auto reads = eventIt.value().toObject().value("m.read"_ls).toObject(); QVector receipts; receipts.reserve(reads.size()); for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) { - const QJsonObject user = userIt.value().toObject(); + const auto user = userIt.value().toObject(); receipts.push_back( { userIt.key(), fromJson(user["ts"_ls]) }); } - _eventsWithReceipts.push_back({ eventIt.key(), std::move(receipts) }); + result.push_back({ eventIt.key(), std::move(receipts) }); } + return result; } diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index dd54a476..b7adea44 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -37,15 +37,9 @@ using EventsWithReceipts = QVector; class ReceiptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) - explicit ReceiptEvent(const QJsonObject& obj); + explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {} - const EventsWithReceipts& eventsWithReceipts() const - { - return _eventsWithReceipts; - } - -private: - EventsWithReceipts _eventsWithReceipts; + EventsWithReceipts eventsWithReceipts() const; }; REGISTER_EVENT_TYPE(ReceiptEvent) } // namespace Quotient diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index fadfece0..4a21b7cc 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -61,13 +61,13 @@ public: explicit RoomCanonicalAliasEvent(const QString& canonicalAlias, const QStringList& altAliases = {}) - : StateEvent(typeId(), matrixTypeId(), QString(), + : StateEvent(typeId(), matrixTypeId(), {}, canonicalAlias, altAliases) { } explicit RoomCanonicalAliasEvent(QString&& canonicalAlias, QStringList&& altAliases = {}) - : StateEvent(typeId(), matrixTypeId(), QString(), + : StateEvent(typeId(), matrixTypeId(), {}, std::move(canonicalAlias), std::move(altAliases)) { } diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3d87ef18..a2dbc07d 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -34,9 +34,8 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { - const auto unsignedData = json[UnsignedKeyL].toObject(); - const auto redaction = unsignedData[RedactedCauseKeyL]; - if (redaction.isObject()) + if (const auto redaction = unsignedJson()[RedactedCauseKeyL]; + redaction.isObject()) _redactedBecause = makeEvent(redaction.toObject()); } diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 3a781474..b8cd2eae 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -8,7 +8,7 @@ class RoomKeyEvent : public Event public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) - RoomKeyEvent(const QJsonObject& obj); + explicit RoomKeyEvent(const QJsonObject& obj); QString algorithm() const { return content("algorithm"_ls); } QString roomId() const { return content(RoomIdKeyL); } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 35fd69a9..cebaaf10 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -54,13 +54,10 @@ public: DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) using MembershipType = MemberEventContent::MembershipType; + Q_ENUM(MembershipType) explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]] - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), QString(), c) - {} template RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) : StateEvent(typeId(), matrixTypeId(), userId, @@ -82,7 +79,7 @@ public: {} MembershipType membership() const { return content().membership; } - QString userId() const { return fullJson()[StateKeyKeyL].toString(); } + QString userId() const { return stateKey(); } bool isDirect() const { return content().isDirect; } Omittable newDisplayName() const { return content().displayName; } Omittable newAvatarUrl() const { return content().avatarUrl; } @@ -104,9 +101,6 @@ public: bool isLeave() const; bool isRename() const; bool isAvatarUpdate() const; - -private: - Q_ENUM(MembershipType) }; template <> diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp index 0c5fc6ba..e102fc79 100644 --- a/lib/events/typingevent.cpp +++ b/lib/events/typingevent.cpp @@ -18,13 +18,9 @@ #include "typingevent.h" -#include - using namespace Quotient; -TypingEvent::TypingEvent(const QJsonObject& obj) : Event(typeId(), obj) +QStringList TypingEvent::users() const { - const auto& array = contentJson()["user_ids"_ls].toArray(); - for (const auto& user : array) - _users.push_back(user.toString()); + return fromJson(contentJson()["user_ids"_ls]); } diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 1cf4e69d..97e1f9cc 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -25,12 +25,9 @@ class TypingEvent : public Event { public: DEFINE_EVENT_TYPEID("m.typing", TypingEvent) - TypingEvent(const QJsonObject& obj); + explicit TypingEvent(const QJsonObject& obj) : Event(typeId(), obj) {} - const QStringList& users() const { return _users; } - -private: - QStringList _users; + QStringList users() const; }; REGISTER_EVENT_TYPE(TypingEvent) } // namespace Quotient 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/events/encryptionevent.cpp | 19 +++++-------------- lib/room.cpp | 8 +++++++- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 073303b0..f1bde621 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -1,21 +1,14 @@ -// -// Created by rusakov on 26/09/2017. -// Contributed by andreev on 27/06/2019. -// - #include "encryptionevent.h" -#include "converters.h" #include "e2ee.h" -#include "logging.h" #include +namespace Quotient { static const std::array encryptionStrings = { - { Quotient::MegolmV1AesSha2AlgoKey } + { MegolmV1AesSha2AlgoKey } }; -namespace Quotient { template <> struct JsonConverter { static EncryptionType load(const QJsonValue& jv) @@ -26,7 +19,8 @@ struct JsonConverter { if (encryptionString == *it) return EncryptionType(it - encryptionStrings.begin()); - qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + if (!encryptionString.isEmpty()) + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; return EncryptionType::Undefined; } }; @@ -35,7 +29,7 @@ struct JsonConverter { using namespace Quotient; EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) - : encryption(fromJson(json["algorithm"_ls])) + : encryption(fromJson(json[AlgorithmKeyL])) , algorithm(sanitized(json[AlgorithmKeyL].toString())) , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000)) , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) @@ -44,9 +38,6 @@ EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) void EncryptionEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); - Q_ASSERT_X( - encryption != EncryptionType::Undefined, __FUNCTION__, - "The key 'algorithm' must be explicit in EncryptionEventContent"); if (encryption != EncryptionType::Undefined) o->insert(AlgorithmKey, algorithm); o->insert(RotationPeriodMsKey, rotationPeriodMs); 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 36d3bf193617f30355bb02c9b038ec9dd6ca123d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 24 Nov 2020 16:50:31 +0100 Subject: CMakeLists.txt: enable generating csapi/search.* Now that the logic behind producing the C++ structures changed (they are filled in on the fly, rather than created upon job completion and then copied/moved by the accessor), GTAD makes buildable code for search.yaml. --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d3bede48..9d5d286d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -211,7 +211,6 @@ if (MATRIX_DOC_PATH AND GTAD_PATH) ${ABS_GTAD_PATH} --config ../gtad/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} old_sync.yaml- room_initial_sync.yaml- # deprecated - search.yaml- # current GTAD is limited in handling move-only data key_backup.yaml- # immature and buggy in terms of API definition sync.yaml- # we have a better handcrafted implementation WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/lib -- cgit v1.2.3 From 21554bccc39ab2f63869c244ed880686805887cb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 24 Nov 2020 16:50:46 +0100 Subject: csapi/search.* --- lib/csapi/search.cpp | 28 +++++ lib/csapi/search.h | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 lib/csapi/search.cpp create mode 100644 lib/csapi/search.h diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp new file mode 100644 index 00000000..5649d52a --- /dev/null +++ b/lib/csapi/search.cpp @@ -0,0 +1,28 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "search.h" + +#include + +using namespace Quotient; + +auto queryToSearch(const QString& nextBatch) +{ + BaseJob::Query _q; + addParam(_q, QStringLiteral("next_batch"), nextBatch); + return _q; +} + +SearchJob::SearchJob(const Categories& searchCategories, + const QString& nextBatch) + : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), + QStringLiteral("/_matrix/client/r0") % "/search", + queryToSearch(nextBatch)) +{ + QJsonObject _data; + addParam<>(_data, QStringLiteral("search_categories"), searchCategories); + setRequestData(std::move(_data)); + addExpectedKey("search_categories"); +} diff --git a/lib/csapi/search.h b/lib/csapi/search.h new file mode 100644 index 00000000..c009ded6 --- /dev/null +++ b/lib/csapi/search.h @@ -0,0 +1,306 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/definitions/room_event_filter.h" + +#include "events/eventloader.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Perform a server-side search. + * + * Performs a full text search across different categories. + */ +class SearchJob : public BaseJob { +public: + // Inner data structures + + /// Configures whether any context for the events + /// returned are included in the response. + struct IncludeEventContext { + /// How many events before the result are + /// returned. By default, this is ``5``. + Omittable beforeLimit; + /// How many events after the result are + /// returned. By default, this is ``5``. + Omittable afterLimit; + /// Requests that the server returns the + /// historic profile information for the users + /// that sent the events that were returned. + /// By default, this is ``false``. + Omittable includeProfile; + }; + + /// Configuration for group. + struct Group { + /// Key that defines the group. + QString key; + }; + + /// Requests that the server partitions the result set + /// based on the provided list of keys. + struct Groupings { + /// List of groups to request. + QVector groupBy; + }; + + /// Mapping of category name to search criteria. + struct RoomEventsCriteria { + /// The string to search events for + QString searchTerm; + /// The keys to search. Defaults to all. + QStringList keys; + /// This takes a `filter`_. + RoomEventFilter filter; + /// The order in which to search for results. + /// By default, this is ``"rank"``. + QString orderBy; + /// Configures whether any context for the events + /// returned are included in the response. + Omittable eventContext; + /// Requests the server return the current state for + /// each room returned. + Omittable includeState; + /// Requests that the server partitions the result set + /// based on the provided list of keys. + Omittable groupings; + }; + + /// Describes which categories to search in and their criteria. + struct Categories { + /// Mapping of category name to search criteria. + Omittable roomEvents; + }; + + /// Performs a full text search across different categories. + struct UserProfile { + /// Performs a full text search across different categories. + QString displayname; + /// Performs a full text search across different categories. + QString avatarUrl; + }; + + /// Context for result, if requested. + struct EventContext { + /// Pagination token for the start of the chunk + QString begin; + /// Pagination token for the end of the chunk + QString end; + /// The historic profile information of the + /// users that sent the events returned. + /// + /// The ``string`` key is the user ID for which + /// the profile belongs to. + QHash profileInfo; + /// Events just before the result. + RoomEvents eventsBefore; + /// Events just after the result. + RoomEvents eventsAfter; + }; + + /// The result object. + struct Result { + /// A number that describes how closely this result matches the search. + /// Higher is closer. + Omittable rank; + /// The event that matched. + RoomEventPtr result; + /// Context for result, if requested. + Omittable context; + }; + + /// The results for a particular group value. + struct GroupValue { + /// Token that can be used to get the next batch + /// of results in the group, by passing as the + /// `next_batch` parameter to the next call. If + /// this field is absent, there are no more + /// results in this group. + QString nextBatch; + /// Key that can be used to order different + /// groups. + Omittable order; + /// Which results are in this group. + QStringList results; + }; + + /// Mapping of category name to search criteria. + struct ResultRoomEvents { + /// An approximate count of the total number of results found. + Omittable count; + /// List of words which should be highlighted, useful for stemming which + /// may change the query terms. + QStringList highlights; + /// List of results in the requested order. + std::vector results; + /// The current state for every room in the results. + /// This is included if the request had the + /// ``include_state`` key set with a value of ``true``. + /// + /// The ``string`` key is the room ID for which the ``State + /// Event`` array belongs to. + UnorderedMap state; + /// Any groups that were requested. + /// + /// The outer ``string`` key is the group key requested (eg: ``room_id`` + /// or ``sender``). The inner ``string`` key is the grouped value (eg: + /// a room's ID or a user's ID). + QHash> groups; + /// Token that can be used to get the next batch of + /// results, by passing as the `next_batch` parameter to + /// the next call. If this field is absent, there are no + /// more results. + QString nextBatch; + }; + + /// Describes which categories to search in and their criteria. + struct ResultCategories { + /// Mapping of category name to search criteria. + Omittable roomEvents; + }; + + // Construction/destruction + + /*! \brief Perform a server-side search. + * + * \param searchCategories + * Describes which categories to search in and their criteria. + * + * \param nextBatch + * The point to return events from. If given, this should be a + * ``next_batch`` result from a previous call to this endpoint. + */ + explicit SearchJob(const Categories& searchCategories, + const QString& nextBatch = {}); + + // Result properties + + /// Describes which categories to search in and their criteria. + ResultCategories searchCategories() const + { + return loadFromJson("search_categories"_ls); + } +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, + const SearchJob::IncludeEventContext& pod) + { + addParam(jo, QStringLiteral("before_limit"), + pod.beforeLimit); + addParam(jo, QStringLiteral("after_limit"), pod.afterLimit); + addParam(jo, QStringLiteral("include_profile"), + pod.includeProfile); + } +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) + { + addParam(jo, QStringLiteral("key"), pod.key); + } +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) + { + addParam(jo, QStringLiteral("group_by"), pod.groupBy); + } +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) + { + addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); + addParam(jo, QStringLiteral("keys"), pod.keys); + addParam(jo, QStringLiteral("filter"), pod.filter); + addParam(jo, QStringLiteral("order_by"), pod.orderBy); + addParam(jo, QStringLiteral("event_context"), + pod.eventContext); + addParam(jo, QStringLiteral("include_state"), + pod.includeState); + addParam(jo, QStringLiteral("groupings"), pod.groupings); + } +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) + { + addParam(jo, QStringLiteral("room_events"), pod.roomEvents); + } +}; + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) + { + fromJson(jo.value("displayname"_ls), result.displayname); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) + { + fromJson(jo.value("start"_ls), result.begin); + fromJson(jo.value("end"_ls), result.end); + fromJson(jo.value("profile_info"_ls), result.profileInfo); + fromJson(jo.value("events_before"_ls), result.eventsBefore); + fromJson(jo.value("events_after"_ls), result.eventsAfter); + } +}; + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) + { + fromJson(jo.value("rank"_ls), result.rank); + fromJson(jo.value("result"_ls), result.result); + fromJson(jo.value("context"_ls), result.context); + } +}; + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) + { + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("order"_ls), result.order); + fromJson(jo.value("results"_ls), result.results); + } +}; + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, + SearchJob::ResultRoomEvents& result) + { + fromJson(jo.value("count"_ls), result.count); + fromJson(jo.value("highlights"_ls), result.highlights); + fromJson(jo.value("results"_ls), result.results); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("groups"_ls), result.groups); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + } +}; + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, + SearchJob::ResultCategories& result) + { + fromJson(jo.value("room_events"_ls), result.roomEvents); + } +}; + +} // namespace Quotient -- cgit v1.2.3 From baabe61cc2e5a2afc00f02ae55465c21b2915bd8 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 14 Nov 2020 14:58:36 +0100 Subject: Add feature summary to cmake file --- CMakeLists.txt | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d3bede48..2b77e262 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,14 @@ endif() set(API_VERSION "0.7") project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX) +include(FeatureSummary) + option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) +add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS "Install Quotest") + # https://github.com/quotient-im/libQuotient/issues/369 option(${PROJECT_NAME}_ENABLE_E2EE "end-to-end encryption (E2EE) support" OFF) +add_feature_info(EnableE2EE ${PROJECT_NAME}_ENABLE_E2EE "Enable end-to-end encryption (E2EE)") include(CheckCXXCompilerFlag) if (WIN32) @@ -59,7 +64,7 @@ else() endforeach () endif() -find_package(Qt5 5.9 REQUIRED Network Gui Multimedia Test) +find_package(Qt5 5.9 REQUIRED Core Network Gui Multimedia Test) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) if (${PROJECT_NAME}_ENABLE_E2EE) @@ -75,15 +80,12 @@ if (${PROJECT_NAME}_ENABLE_E2EE) set(SAVED_CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) set(CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) find_package(QtOlm 3.0.1 REQUIRED) - if (NOT QtOlm_FOUND) - message( WARNING "libQtOlm not found; configuration will most likely fail.") - message( WARNING "Make sure you have installed libQtOlm development files") - message( WARNING "as a package or checked out the library sources in lib/.") - message( WARNING "See also BUILDING.md") - endif () + set_package_properties(QtOlm PROPERTIES + DESCRIPTION "QtOlm is a Qt wrapper around libOlm" + PURPOSE "libQtOlm is required to support end-to-end encryption. See also BUILDING.md" + URL "https://gitlab.com/b0/libqtolm" + ) endif () -else () - message( STATUS "End-to-end encryption (E2EE) support is turned off.") endif () if (GTAD_PATH) @@ -99,11 +101,6 @@ if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM) endif() -message( STATUS ) -message( STATUS "=============================================================================" ) -message( STATUS " ${PROJECT_NAME} Build Information" ) -message( STATUS "=============================================================================" ) -message( STATUS "Version: ${PROJECT_VERSION}, API version: ${API_VERSION}") if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) @@ -341,3 +338,5 @@ if (UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() + +feature_summary(WHAT ALL) -- 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. --- CMakeLists.txt | 2 +- lib/connection.cpp | 4 ++-- lib/events/eventcontent.cpp | 2 +- lib/events/eventcontent.h | 2 +- lib/events/roommemberevent.cpp | 2 +- lib/jobs/basejob.cpp | 2 +- lib/room.cpp | 2 +- lib/ssosession.cpp | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d5d286d..4bb2bd31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -257,7 +257,7 @@ file(GLOB_RECURSE api_SRCS ${add_CONFIGURE_DEPENDS} ${FULL_CSAPI_DIR}/*.cpp) set(tests_SRCS tests/quotest.cpp) add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) -target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS) +target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) diff --git a/lib/connection.cpp b/lib/connection.cpp index e84b8080..2a86d2eb 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -931,8 +931,8 @@ void Connection::doInDirectChat(User* u, // There can be more than one DC; find the first valid (existing and // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; - for (auto it = std::as_const(d->directChats).find(u); - it != d->directChats.end() && it.key() == u; ++it) { + for (auto it = d->directChats.constFind(u); + it != d->directChats.constEnd() && it.key() == u; ++it) { const auto& roomId = *it; if (auto r = room(roomId, JoinState::Join)) { Q_ASSERT(r->id() == roomId); diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 802d8176..0cb9e292 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -89,7 +89,7 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const } Thumbnail::Thumbnail(const QJsonObject& infoJson) - : ImageInfo(infoJson["thumbnail_url"_ls].toString(), + : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), infoJson["thumbnail_info"_ls].toObject()) {} diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 0d4c047e..9c167d4b 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -189,7 +189,7 @@ namespace EventContent { using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) - , InfoT(json["url"].toString(), json["info"].toObject(), + , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), json["filename"].toString()) { // A small hack to facilitate links creation in QML. diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index be47e412..913bde74 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -52,7 +52,7 @@ MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) , displayName(fromJson>(json["displayname"_ls])) - , avatarUrl(fromJson>(json["avatar_url"_ls])) + , avatarUrl(fromJson>(json["avatar_url"_ls])) , reason(json["reason"_ls].toString()) { if (displayName) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 1f0e84ba..422931ee 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -534,7 +534,7 @@ BaseJob::Status BaseJob::prepareError() } if (errCode == "M_CONSENT_NOT_GIVEN") { - d->errorUrl = errorJson.value("consent_uri"_ls).toString(); + d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString()); return { UserConsentRequiredError }; } if (errCode == "M_UNSUPPORTED_ROOM_VERSION" 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())); diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp index be701204..3c6ec48b 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -70,7 +70,7 @@ SsoSession::~SsoSession() QUrl SsoSession::ssoUrl() const { return d->ssoUrl; } -QUrl SsoSession::callbackUrl() const { return d->callbackUrl; } +QUrl SsoSession::callbackUrl() const { return QUrl(d->callbackUrl); } void SsoSession::Private::processCallback() { -- cgit v1.2.3 From 7b2eb5b3a14dd198564658c143567b3eb7a879f6 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 27 Nov 2020 00:58:48 +0100 Subject: Make it compile with QT_NO_KEYWORDS --- lib/connection.h | 6 +++--- lib/jobs/basejob.h | 8 ++++---- lib/room.h | 4 ++-- lib/uriresolver.h | 2 +- lib/user.h | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 6517b909..07ae9f29 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -497,7 +497,7 @@ public: setUserFactory(defaultUserFactory()); } -public slots: +public Q_SLOTS: /** Set the homeserver base URL */ void setHomeserver(const QUrl& baseUrl); @@ -656,7 +656,7 @@ public slots: */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event); -signals: +Q_SIGNALS: /** * @deprecated * This was a signal resulting from a successful resolveServer(). @@ -853,7 +853,7 @@ protected: */ void onSyncSuccess(SyncData&& data, bool fromCache = false); -protected slots: +protected Q_SLOTS: void syncLoopIteration(); private: diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index be2926be..5c054ed1 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -251,7 +251,7 @@ public: return dbg << j->objectName(); } -public slots: +public Q_SLOTS: void initiate(ConnectionData* connData, bool inBackground); /** @@ -263,7 +263,7 @@ public slots: */ void abandon(); -signals: +Q_SIGNALS: /** The job is about to send a network request */ void aboutToSendRequest(); @@ -433,7 +433,7 @@ protected: // Job objects should only be deleted via QObject::deleteLater ~BaseJob() override; -protected slots: +protected Q_SLOTS: void timeout(); /*! \brief Check the pending or received reply for upfront issues @@ -456,7 +456,7 @@ protected slots: */ virtual Status checkReply(const QNetworkReply *reply) const; -private slots: +private Q_SLOTS: void sendRequest(); void gotReply(); diff --git a/lib/room.h b/lib/room.h index f4d7eb70..7eee022c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -547,7 +547,7 @@ public: return setState(EvT(std::forward(args)...)); } -public slots: +public Q_SLOTS: /** Check whether the room should be upgraded */ void checkVersion(); @@ -611,7 +611,7 @@ public slots: void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); -signals: +Q_SIGNALS: /// Initial set of state events has been loaded /** * The initial set is what comes from the initial sync for the room. diff --git a/lib/uriresolver.h b/lib/uriresolver.h index 9b2ced9d..428ce04c 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -141,7 +141,7 @@ public: return UriResolverBase::visitResource(account, uri); } -signals: +Q_SIGNALS: /// An action on a user has been requested void userAction(Quotient::User* user, QString action); diff --git a/lib/user.h b/lib/user.h index a3b22480..19f57c30 100644 --- a/lib/user.h +++ b/lib/user.h @@ -122,7 +122,7 @@ public: QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; -public slots: +public Q_SLOTS: /// Set a new name in the global user profile void rename(const QString& newName); /// Set a new name for the user in one room @@ -143,7 +143,7 @@ public slots: /// Check whether the user is in ignore list bool isIgnored() const; -signals: +Q_SIGNALS: void defaultNameChanged(); void defaultAvatarChanged(); -- cgit v1.2.3 From 57c46baaea2384c1caed46033f6bdb3c5c3a75da Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Dec 2020 20:19:46 +0100 Subject: CMakeLists.txt: reworked configuration logging Notably, screen-wide ==== fences are gone, and the status messages are now located next to where the relevant piece of configuration occurs, instead of having a configuration summary block. Also, features related to code generation have been added for feature_summary(). --- CMakeLists.txt | 127 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2773c9da..393ee94f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,38 +6,19 @@ endif() set(API_VERSION "0.7") project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX) +message(STATUS) +message(STATUS "Configuring ${PROJECT_NAME} ${PROJECT_VERSION} ==>") + include(FeatureSummary) option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) -add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS "Install Quotest") +add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS + "the library functional test suite") # https://github.com/quotient-im/libQuotient/issues/369 option(${PROJECT_NAME}_ENABLE_E2EE "end-to-end encryption (E2EE) support" OFF) -add_feature_info(EnableE2EE ${PROJECT_NAME}_ENABLE_E2EE "Enable end-to-end encryption (E2EE)") - -include(CheckCXXCompilerFlag) -if (WIN32) - if (NOT CMAKE_INSTALL_LIBDIR) - set(CMAKE_INSTALL_LIBDIR ".") - endif () - - if (NOT CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR ".") - endif () - - if (NOT CMAKE_INSTALL_INCLUDEDIR) - set(CMAKE_INSTALL_INCLUDEDIR "include") - endif () -else() - include(GNUInstallDirs) - set(INCLUDEDIR_INIT ${PROJECT_NAME}) -endif(WIN32) -set(${PROJECT_NAME}_INSTALL_INCLUDEDIR - "${CMAKE_INSTALL_INCLUDEDIR}/${INCLUDEDIR_INIT}" CACHE PATH - "directory to install ${PROJECT_NAME} include files to") - -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) +add_feature_info(EnableE2EE ${PROJECT_NAME}_ENABLE_E2EE + "end-to-end encryption (WORK IN PROGRESS)") # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) @@ -47,7 +28,12 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() +if (CMAKE_BUILD_TYPE) + message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") +endif(CMAKE_BUILD_TYPE) +message(STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) +include(CheckCXXCompilerFlag) if (MSVC) add_compile_options(/EHsc /W4 /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 @@ -64,8 +50,34 @@ else() endforeach () endif() +if (WIN32) + if (NOT CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR ".") + endif () + + if (NOT CMAKE_INSTALL_BINDIR) + set(CMAKE_INSTALL_BINDIR ".") + endif () + + if (NOT CMAKE_INSTALL_INCLUDEDIR) + set(CMAKE_INSTALL_INCLUDEDIR "include") + endif () +else() + include(GNUInstallDirs) + set(INCLUDEDIR_INIT ${PROJECT_NAME}) +endif(WIN32) +set(${PROJECT_NAME}_INSTALL_INCLUDEDIR + "${CMAKE_INSTALL_INCLUDEDIR}/${INCLUDEDIR_INIT}" CACHE PATH + "directory to install ${PROJECT_NAME} include files to") +message(STATUS "Install Prefix: ${CMAKE_INSTALL_PREFIX}") +message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${PROJECT_NAME}_INSTALL_INCLUDEDIR}") + +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) + find_package(Qt5 5.9 REQUIRED Core Network Gui Multimedia Test) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) +message(STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}") if (${PROJECT_NAME}_ENABLE_E2EE) if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) @@ -76,7 +88,18 @@ if (${PROJECT_NAME}_ENABLE_E2EE) set (USE_INTREE_LIBQOLM 1) endif () endif () - if (NOT USE_INTREE_LIBQOLM) + if (USE_INTREE_LIBQOLM) + message( STATUS "Using in-tree libQtOlm") + find_package(Git QUIET) + if (GIT_FOUND) + execute_process(COMMAND + "${GIT_EXECUTABLE}" rev-parse -q HEAD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm + OUTPUT_VARIABLE QTOLM_GIT_SHA1 + OUTPUT_STRIP_TRAILING_WHITESPACE) + message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") + endif (GIT_FOUND) + else () set(SAVED_CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) set(CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) find_package(QtOlm 3.0.1 REQUIRED) @@ -85,6 +108,9 @@ if (${PROJECT_NAME}_ENABLE_E2EE) PURPOSE "libQtOlm is required to support end-to-end encryption. See also BUILDING.md" URL "https://gitlab.com/b0/libqtolm" ) + if (QtOlm_FOUND) + message(STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") + endif() endif () endif () @@ -95,47 +121,28 @@ if (MATRIX_DOC_PATH) get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) endif () if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) + message( STATUS "Using GTAD at ${ABS_GTAD_PATH}" ) + message( STATUS "Using API files at ${ABS_API_DEF_PATH}" ) + set(API_GENERATION_ENABLED 1) if (NOT CLANG_FORMAT) set(CLANG_FORMAT clang-format) endif() get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM) -endif() - -if (CMAKE_BUILD_TYPE) - message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") -endif(CMAKE_BUILD_TYPE) -message( STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) -message( STATUS "Install Prefix: ${CMAKE_INSTALL_PREFIX}" ) -message( STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${PROJECT_NAME}_INSTALL_INCLUDEDIR}" ) -message( STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}" ) -if (ABS_API_DEF_PATH AND ABS_GTAD_PATH) - message( STATUS "Generating API stubs enabled (use --target update-api)" ) - message( STATUS " Using GTAD at ${ABS_GTAD_PATH}" ) - message( STATUS " Using API files at ${ABS_API_DEF_PATH}" ) if (ABS_CLANG_FORMAT) + set(API_FORMATTING_ENABLED 1) message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") else () message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") endif () -endif () -find_package(Git) -if (${PROJECT_NAME}_ENABLE_E2EE) - if (USE_INTREE_LIBQOLM) - message( STATUS "Using in-tree libQtOlm") - if (GIT_FOUND) - execute_process(COMMAND - "${GIT_EXECUTABLE}" rev-parse -q HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm - OUTPUT_VARIABLE QTOLM_GIT_SHA1 - OUTPUT_STRIP_TRAILING_WHITESPACE) - message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") - endif (GIT_FOUND) - else () - message( STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") - endif () -endif () -message( STATUS "=============================================================================" ) -message( STATUS ) +endif() +add_feature_info(EnableApiCodeGeneration ${API_GENERATION_ENABLED} + "build target update-api") +add_feature_info(EnableApiFormatting ${API_FORMATTING_ENABLED} + "formatting of generated API files with clang-format") + +message(STATUS) +feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES + FATAL_ON_MISSING_REQUIRED_PACKAGES) # Set up source files set(lib_SRCS @@ -338,4 +345,4 @@ if (UNIX AND NOT APPLE) DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() -feature_summary(WHAT ALL) +message(STATUS "<== End of libQuotient configuration") -- 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(-) 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 f160c92d06446bd90b8ac4e1d238ecabb0c97651 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Dec 2020 22:13:08 +0100 Subject: CMakeLists.txt: fix configuration failures --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 393ee94f..eb3f9528 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,9 +135,9 @@ if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") endif () endif() -add_feature_info(EnableApiCodeGeneration ${API_GENERATION_ENABLED} +add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" "build target update-api") -add_feature_info(EnableApiFormatting ${API_FORMATTING_ENABLED} +add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" "formatting of generated API files with clang-format") message(STATUS) -- cgit v1.2.3 From 6ac7d7b77242b36d7fc52ea84a061464c71579c7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 10 Dec 2020 01:53:04 +0100 Subject: Fix DELETE jobs with json data DeleteDeviceJob requires authentication, but the JSON data is not added for DELETE requests. Since QNetworkAccessManager::deleteResource does not support body data, we need to send a custom request. --- lib/jobs/basejob.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 1f0e84ba..b757854a 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -324,7 +324,7 @@ void BaseJob::Private::sendRequest() reply.reset(connection->nam()->put(req, requestData.source())); break; case HttpVerb::Delete: - reply.reset(connection->nam()->deleteResource(req)); + reply.reset(connection->nam()->sendCustomRequest(req, "DELETE", requestData.source())); break; } } -- cgit v1.2.3 From 903e309c51cdbe9b29131a1ce1a24facfd51de9e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 10 Dec 2020 17:34:10 +0100 Subject: Uri: fix a few cases of insufficient escaping --- lib/uri.cpp | 64 +++++++++++++++++++++++++++++++++++++++---------------- tests/quotest.cpp | 17 +++++++++------ 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/lib/uri.cpp b/lib/uri.cpp index 8370d83a..0e2bcb87 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -30,7 +30,9 @@ Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) for (const auto& p: replacePairs) if (primaryId[0] == p.sigil) { primaryType_ = Type(p.sigil); - pathToBe = p.uriString + primaryId.mid(1); + auto safePrimaryId = primaryId.mid(1); + safePrimaryId.replace('/', "%2F"); + pathToBe = p.uriString + std::move(safePrimaryId); break; } if (!secondaryId.isEmpty()) { @@ -38,11 +40,40 @@ Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) primaryType_ = Invalid; return; } - pathToBe += "/event/" + secondaryId.mid(1); + auto safeSecondaryId = secondaryId.mid(1); + safeSecondaryId.replace('/', "%2F"); + pathToBe += "/event/" + std::move(safeSecondaryId); } - setPath(pathToBe); + setPath(pathToBe, QUrl::TolerantMode); } - setQuery(std::move(query)); + if (!query.isEmpty()) + setQuery(std::move(query)); +} + +static inline auto encodedPath(const QUrl& url) +{ + return url.path(QUrl::EncodeDelimiters | QUrl::EncodeUnicode); +} + +static QString pathSegment(const QUrl& url, int which) +{ + return QUrl::fromPercentEncoding( + encodedPath(url).section('/', which, which).toUtf8()); +} + +static auto decodeFragmentPart(const QStringRef& part) +{ + return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); +} + +static auto matrixToUrlRegexInit() +{ + // See https://matrix.org/docs/spec/appendices#matrix-to-navigation + const QRegularExpression MatrixToUrlRE { + "^/(?
[^:]+:[^/?]+)(/(?(\\$|%24)[^?]+))?(\\?(?.+))?$" + }; + Q_ASSERT(MatrixToUrlRE.isValid()); + return MatrixToUrlRE; } Uri::Uri(QUrl url) : QUrl(std::move(url)) @@ -51,14 +82,13 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) if (isEmpty()) return; // primaryType_ == Empty - if (!QUrl::isValid()) { // MatrixUri::isValid() checks primaryType_ - primaryType_ = Invalid; + primaryType_ = Invalid; + if (!QUrl::isValid()) // MatrixUri::isValid() checks primaryType_ return; - } if (scheme() == "matrix") { // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312 - const auto& urlPath = path(); + const auto& urlPath = encodedPath(*this); const auto& splitPath = urlPath.splitRef('/'); switch (splitPath.size()) { case 2: @@ -83,17 +113,15 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) primaryType_ = NonMatrix; // Default, unless overridden by the code below if (scheme() == "https" && authority() == "matrix.to") { - // See https://matrix.org/docs/spec/appendices#matrix-to-navigation - static const QRegularExpression MatrixToUrlRE { - R"(^/(?
[^/?]+)(/(?[^?]+))?(\?(?.+))?$)" - }; + static const auto MatrixToUrlRE = matrixToUrlRegexInit(); // matrix.to accepts both literal sigils (as well as & and ? used in // its "query" substitute) and their %-encoded forms; // so force QUrl to decode everything. - auto f = fragment(QUrl::FullyDecoded); + auto f = fragment(QUrl::EncodeUnicode); if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch()) - *this = Uri { m.captured("main").toUtf8(), - m.captured("sec").toUtf8(), m.captured("query") }; + *this = Uri { decodeFragmentPart(m.capturedRef("main")), + decodeFragmentPart(m.capturedRef("sec")), + decodeFragmentPart(m.capturedRef("query")) }; } } @@ -119,7 +147,7 @@ Uri::Type Uri::type() const { return primaryType_; } Uri::SecondaryType Uri::secondaryType() const { - return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId; + return pathSegment(*this, 2) == "event" ? EventId : NoSecondaryId; } QUrl Uri::toUrl(UriForm form) const @@ -148,13 +176,13 @@ QString Uri::primaryId() const if (primaryType_ == Empty || primaryType_ == Invalid) return {}; - const auto& idStem = path().section('/', 1, 1); + const auto& idStem = pathSegment(*this, 1); return idStem.isEmpty() ? idStem : primaryType_ + idStem; } QString Uri::secondaryId() const { - const auto& idStem = path().section('/', 3); + const auto& idStem = pathSegment(*this, 3); return idStem.isEmpty() ? idStem : secondaryType() + idStem; } diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 3b7f2f48..9930a3ab 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -727,14 +727,19 @@ TEST_IMPL(visitResources) "matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1), "https://matrix.to/#/" + roomId + '/' + eventId }; - // The following URIs are not supposed to be actually joined (and even - // exist, as yet) - only to be syntactically correct + // Check that reserved characters are correctly processed. static const auto& joinRoomAlias = - QStringLiteral("unjoined:example.org"); // # will be added below + QStringLiteral("##/?.@\"unjoined:example.org"); + static const auto& encodedRoomAliasNoSigil = + QUrl::toPercentEncoding(joinRoomAlias.mid(1), ":"); static const QString joinQuery { "?action=join" }; + // These URIs are not supposed to be actually joined (and even exist, + // as yet) - only to be syntactically correct static const QStringList joinByAliasUris { - "matrix:room/" + joinRoomAlias + joinQuery, - "https://matrix.to/#/%23"/*`#`*/ + joinRoomAlias + joinQuery + Uri(joinRoomAlias.toUtf8(), {}, joinQuery.mid(1)).toDisplayString(), + "matrix:room/" + encodedRoomAliasNoSigil + joinQuery, + "https://matrix.to/#/%23"/*`#`*/ + encodedRoomAliasNoSigil + joinQuery, + "https://matrix.to/#/%23" + joinRoomAlias.mid(1) /* unencoded */ + joinQuery }; static const auto& joinRoomId = QStringLiteral("!anyid:example.org"); static const QStringList viaServers { "matrix.org", "example.org" }; @@ -755,7 +760,7 @@ TEST_IMPL(visitResources) || testResourceResolver(eventUris, &UriDispatcher::roomAction, room(), { eventId }) || testResourceResolver(joinByAliasUris, &UriDispatcher::joinAction, - connection(), { '#' + joinRoomAlias }) + connection(), { joinRoomAlias }) || testResourceResolver(joinByIdUris, &UriDispatcher::joinAction, connection(), { joinRoomId, viaServers })) return true; -- cgit v1.2.3 From 3ef036cd5936df4c0324ef54aa2bc11d745bd4c7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 23 Dec 2020 17:18:12 +0100 Subject: Connection::resolveServer(): fix error handling Part of the fix for #421. (cherry picked from commit 104356d945671762af23e346f7898a3208770d97) --- lib/connection.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index e84b8080..386c6564 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -270,8 +270,8 @@ void Connection::resolveServer(const QString& mxid) d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file d->resolverJob = callApi(); connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl] { - if (d->resolverJob->status() != BaseJob::NotFoundError) { - if (d->resolverJob->status() != BaseJob::Success) { + if (d->resolverJob->error() != BaseJob::NotFoundError) { + if (!d->resolverJob->status().good()) { qCWarning(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; emit resolveError(tr("Failed resolving the homeserver")); @@ -301,8 +301,7 @@ void Connection::resolveServer(const QString& mxid) &Connection::resolved); connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { qCWarning(MAIN) << "Homeserver base URL sanity check failed"; - emit resolveError( - tr("The homeserver base URL doesn't seem to work")); + emit resolveError(tr("The homeserver doesn't seem to be working")); }); }); } -- cgit v1.2.3 From 374377c37e8b82f1503fbb259e529f6551716df3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 23 Dec 2020 17:21:01 +0100 Subject: BaseJob: tolerate unexpected error payloads Proxy servers may return arbitrary HTML, for one example; so don't expect to find a valid JSON object in whatever non-empty payload next to a non-2xx HTTP code. Fixes #421. (cherry picked from commit 9ef83e044ed4f8409156b19d529dfc7e45f565c1) --- lib/jobs/basejob.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b757854a..e16ba4ef 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -510,14 +510,15 @@ BaseJob::Status BaseJob::prepareResult() { return Success; } BaseJob::Status BaseJob::prepareError() { - if (!d->rawResponse.isEmpty()) { - if (const auto status = d->parseJson(); !status.good()) - return status; // If there's anything there, it should be JSON - if (d->jsonResponse.isArray()) // ...specifically, a JSON object - return { IncorrectResponse, - tr("Malformed error JSON: an array instead of an object") }; - } - + // Try to make sense of the error payload but be prepared for all kinds + // of unexpected stuff (raw HTML, plain text, foreign JSON among those) + if (!d->rawResponse.isEmpty() + && reply()->rawHeader("Content-Type") == "application/json") + d->parseJson(); + + // By now, if d->parseJson() above succeeded then jsonData() will return + // a valid JSON object - or an empty object otherwise (in which case most + // of if's below will fall through to `return NoError` at the end const auto& errorJson = jsonData(); const auto errCode = errorJson.value("errcode"_ls).toString(); if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { -- cgit v1.2.3 From 84b21debc68b16b7dad7f6ed2e679e1c224956ea Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 23 Dec 2020 17:21:44 +0100 Subject: BaseJob: add [[fallthrough]] as clang-tidy says (cherry picked from commit 1a832ae9b6a0d679b551fd644136e4bc17e7db29) --- lib/jobs/basejob.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index e16ba4ef..3fa1cd94 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -620,6 +620,7 @@ void BaseJob::finishJob() emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); return; } + [[fallthrough]]; default:; } -- cgit v1.2.3 From 3bfe40e40183821557845456349e1079af7b4e25 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 23 Dec 2020 17:29:25 +0100 Subject: BaseJob::Status: add comparison with int Since Status single-parameter constructor is (intentionally) not explicit, comparisons may not do what's expected in cases like the one fixed by 3ef036cd. This makes comparisons "do the right thing". --- lib/jobs/basejob.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 5c054ed1..a0b89ef7 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -136,6 +136,14 @@ public: { return !operator==(other); } + bool operator==(int otherCode) const + { + return code == otherCode; + } + bool operator!=(int otherCode) const + { + return !operator==(otherCode); + } int code; QString message; -- 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/connection.cpp | 4 ++-- lib/converters.cpp | 4 ++-- lib/jobs/downloadfilejob.cpp | 2 +- lib/networkaccessmanager.cpp | 7 ++++--- lib/room.cpp | 49 ++++++++++++++++++++++---------------------- lib/uri.cpp | 8 ++++---- lib/uriresolver.cpp | 2 ++ lib/user.cpp | 4 ++-- lib/util.cpp | 5 +---- tests/quotest.cpp | 38 +++++++++++++++++++--------------- 10 files changed, 64 insertions(+), 59 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 386c6564..b76ca691 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -930,8 +930,8 @@ void Connection::doInDirectChat(User* u, // There can be more than one DC; find the first valid (existing and // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; - for (auto it = std::as_const(d->directChats).find(u); - it != d->directChats.end() && it.key() == u; ++it) { + for (auto it = d->directChats.constFind(u); + it != d->directChats.cend() && it.key() == u; ++it) { const auto& roomId = *it; if (auto r = room(roomId, JoinState::Join)) { Q_ASSERT(r->id() == roomId); diff --git a/lib/converters.cpp b/lib/converters.cpp index e5236bb9..9f570087 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -32,9 +32,9 @@ QVariant JsonConverter::load(const QJsonValue& jv) return jv.toVariant(); } -QJsonObject JsonConverter::dump(const QVariantHash& map) +QJsonObject JsonConverter::dump(const QVariantHash& vh) { - return QJsonObject::fromVariantHash(map); + return QJsonObject::fromVariantHash(vh); } QVariantHash JsonConverter::load(const QJsonValue& jv) diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 7b4cf690..0011a97c 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -64,7 +64,7 @@ void DownloadFileJob::onSentRequest(QNetworkReply* reply) return; auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); if (sizeHeader.isValid()) { - auto targetSize = sizeHeader.value(); + auto targetSize = sizeHeader.toLongLong(); if (targetSize != -1) if (!d->tempFile->resize(targetSize)) { qCWarning(JOBS) << "Failed to allocate" << targetSize diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index b9037bcc..e8aa85df 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -52,9 +52,10 @@ static NetworkAccessManager* createNam() auto nam = new NetworkAccessManager(QCoreApplication::instance()); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) // See #109; in newer Qt, bearer management is deprecated altogether - nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, [nam] { - nam->setNetworkAccessible(QNetworkAccessManager::Accessible); - }); + NetworkAccessManager::connect(nam, + &QNetworkAccessManager::networkAccessibleChanged, [nam] { + nam->setNetworkAccessible(QNetworkAccessManager::Accessible); + }); #endif return nam; } 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) { diff --git a/lib/uri.cpp b/lib/uri.cpp index 0e2bcb87..e0912eb6 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -32,7 +32,7 @@ Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) primaryType_ = Type(p.sigil); auto safePrimaryId = primaryId.mid(1); safePrimaryId.replace('/', "%2F"); - pathToBe = p.uriString + std::move(safePrimaryId); + pathToBe = p.uriString + safePrimaryId; break; } if (!secondaryId.isEmpty()) { @@ -42,12 +42,12 @@ Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) } auto safeSecondaryId = secondaryId.mid(1); safeSecondaryId.replace('/', "%2F"); - pathToBe += "/event/" + std::move(safeSecondaryId); + pathToBe += "/event/" + safeSecondaryId; } setPath(pathToBe, QUrl::TolerantMode); } if (!query.isEmpty()) - setQuery(std::move(query)); + setQuery(query); } static inline auto encodedPath(const QUrl& url) @@ -156,7 +156,7 @@ QUrl Uri::toUrl(UriForm form) const return {}; if (form == CanonicalUri || type() == NonMatrix) - return *this; + return *this; // NOLINT(cppcoreguidelines-slicing): It's intentional QUrl url; url.setScheme("https"); diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp index ec30512c..27360bcc 100644 --- a/lib/uriresolver.cpp +++ b/lib/uriresolver.cpp @@ -75,6 +75,8 @@ private: std::tuple fns_; }; +template +StaticUriDispatcher(FnTs&&... fns) -> StaticUriDispatcher; UriResolveResult Quotient::visitResource( Connection* account, const Uri& uri, diff --git a/lib/user.cpp b/lib/user.cpp index 45a9c121..c399ce88 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -78,7 +78,7 @@ QString User::id() const { return d->id; } bool User::isGuest() const { Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@')); - auto it = std::find_if_not(d->id.begin() + 1, d->id.end(), + auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(), [](QChar c) { return c.isDigit(); }); Q_ASSERT(it != d->id.end()); return *it == ':'; @@ -138,7 +138,7 @@ inline bool User::doSetAvatar(SourceT&& source) connect(j, &BaseJob::success, this, [this, newUrl = QUrl(contentUri)] { if (newUrl == d->defaultAvatar.url()) { - d->defaultAvatar.updateUrl(move(newUrl)); + d->defaultAvatar.updateUrl(newUrl); emit defaultAvatarChanged(); } else qCWarning(MAIN) << "User" << id() diff --git a/lib/util.cpp b/lib/util.cpp index 0c1c54ff..36015d7a 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -161,9 +161,6 @@ static_assert(std::is_same, int>(), "Test fn_arg_t defaulting to first argument"); template -static QString ft(T&&) -{ - return {}; -} +static QString ft(T&&); static_assert(std::is_same)>, QString&&>(), "Test function templates"); diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 9930a3ab..d4bfa786 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -104,17 +104,21 @@ private slots: // Add more tests above here public: - Room* room() const { return targetRoom; } - Connection* connection() const { return targetRoom->connection(); } + [[nodiscard]] Room* room() const { return targetRoom; } + [[nodiscard]] Connection* connection() const + { + return targetRoom->connection(); + } private: - bool checkFileSendingOutcome(const TestToken& thisTest, - const QString& txnId, const QString& fileName); - bool checkRedactionOutcome(const QByteArray& thisTest, - const QString& evtIdToRedact); - - bool validatePendingEvent(const QString& txnId); - bool checkDirectChat() const; + [[nodiscard]] bool checkFileSendingOutcome(const TestToken& thisTest, + const QString& txnId, + const QString& fileName); + [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact); + + [[nodiscard]] bool validatePendingEvent(const QString& txnId); + [[nodiscard]] bool checkDirectChat() const; void finishTest(const TestToken& token, bool condition, const char* file, int line); @@ -185,7 +189,7 @@ TestManager::TestManager(int& argc, char** argv) [this](const QString& error) { clog << "Failed to resolve the server: " << error.toStdString() << endl; - this->exit(-2); + QCoreApplication::exit(-2); }, Qt::QueuedConnection); connect(c, &Connection::loginError, this, @@ -195,7 +199,7 @@ TestManager::TestManager(int& argc, char** argv) << message.toStdString() << endl << "Details:" << endl << details.toStdString() << endl; - this->exit(-2); + QCoreApplication::exit(-2); }, Qt::QueuedConnection); connect(c, &Connection::loadedRoomState, this, &TestManager::onNewRoom); @@ -320,7 +324,7 @@ TEST_IMPL(loadMembers) FAIL_TEST(); } r->setDisplayed(); - connect(r, &Room::allMembersLoaded, [this, thisTest, r] { + connect(r, &Room::allMembersLoaded, this, [this, thisTest, r] { FINISH_TEST(r->memberNames().size() >= r->joinedCount()); }); return false; @@ -745,7 +749,7 @@ TEST_IMPL(visitResources) static const QStringList viaServers { "matrix.org", "example.org" }; static const auto viaQuery = std::accumulate(viaServers.cbegin(), viaServers.cend(), joinQuery, - [](QString q, const QString& s) { + [](const QString& q, const QString& s) { return q + "&via=" + s; }); static const QStringList joinByIdUris { @@ -807,7 +811,8 @@ void TestManager::conclude() // Now just wait until all the pending events reach the server connectUntil(room, &Room::messageSent, this, [this, room, plainReport] { const auto& pendingEvents = room->pendingEvents(); - if (auto c = std::count_if(pendingEvents.begin(), pendingEvents.end(), + if (auto c = std::count_if(pendingEvents.cbegin(), + pendingEvents.cend(), [](const PendingEventItem& pe) { return pe.deliveryStatus() < EventStatus::ReachedServer; @@ -834,8 +839,8 @@ void TestManager::finalize() { clog << "Logging out" << endl; c->logout(); - connect(c, &Connection::loggedOut, - this, [this] { this->exit(failed.size() + running.size()); }, + connect(c, &Connection::loggedOut, this, + [this] { QCoreApplication::exit(failed.size() + running.size()); }, Qt::QueuedConnection); } @@ -847,6 +852,7 @@ int main(int argc, char* argv[]) << endl; return -1; } + // NOLINTNEXTLINE(readability-static-accessed-through-instance) return TestManager(argc, argv).exec(); } -- 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(-) 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 34660bed9e90374a1ace0913331ca2965513d19b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 24 Dec 2020 18:23:00 +0100 Subject: quotest: wait until the final report is actually sent Previously the code was waiting until an arbitrary event is sent. (cherry picked from commit 424d5c33542c4c38baefb773163e9ebed1accfb6) --- tests/quotest.cpp | 55 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index d4bfa786..2b1f0229 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -803,36 +803,39 @@ void TestManager::conclude() htmlReport += "
Did not finish:" + dnfList; } - // TODO: Waiting for proper futures to come so that it could be: - // targetRoom->postHtmlText(...) - // .then(this, &TestManager::finalize); // Qt-style or - // .then([this] { finalize(); }); // STL-style auto txnId = room->postHtmlText(plainReport, htmlReport); // Now just wait until all the pending events reach the server - connectUntil(room, &Room::messageSent, this, [this, room, plainReport] { - const auto& pendingEvents = room->pendingEvents(); - if (auto c = std::count_if(pendingEvents.cbegin(), - pendingEvents.cend(), - [](const PendingEventItem& pe) { - return pe.deliveryStatus() - < EventStatus::ReachedServer; - }); - c > 0) { - clog << "Events to reach the server: " << c << ", not leaving yet" - << endl; - return false; - } + connectUntil(room, &Room::messageSent, this, + [this, txnId, room, plainReport] (const QString& sentTxnId) { + if (sentTxnId != txnId) + return false; + const auto& pendingEvents = room->pendingEvents(); + if (auto c = std::count_if(pendingEvents.cbegin(), + pendingEvents.cend(), + [](const PendingEventItem& pe) { + return pe.deliveryStatus() + < EventStatus::ReachedServer; + }); + c > 0) { + clog << "Events to reach the server: " << c + << ", not leaving yet" << endl; + return false; + } - clog << "Leaving the room" << endl; - auto* job = room->leaveRoom(); - connect(job, &BaseJob::finished, this, [this, job,plainReport] { - Q_ASSERT(job->status().good()); - finalize(); - // Still flying, as the exit() connection in finalize() is queued - clog << plainReport.toStdString() << endl; + clog << "Leaving the room" << endl; + // TODO: Waiting for proper futures to come so that it could be: +// room->leaveRoom() +// .then(this, &TestManager::finalize); // Qt-style or +// .then([this] { finalize(); }); // STL-style + auto* job = room->leaveRoom(); + connect(job, &BaseJob::finished, this, [this, job,plainReport] { + Q_ASSERT(job->status().good()); + finalize(); + // Still flying, as the exit() connection in finalize() is queued + clog << plainReport.toStdString() << endl; + }); + return true; }); - return true; - }); } void TestManager::finalize() -- cgit v1.2.3 From d5670a9dea90e111d805ae144c7295cd58b29d22 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 24 Dec 2020 19:27:29 +0100 Subject: Refresh CONTRIBUTING.md [skip ci] (cherry picked from commit de2fa43fc7cc89b4a45cbd885866b5ca747751ef) --- CONTRIBUTING.md | 83 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 281ab5b5..bda004df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,9 +195,10 @@ Additional considerations: `std::deque` for a timeline). Exposing STL containers or iterators in API intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work and therefore unlikely to be accepted into `master`. -* Although `std::unique_ptr<>` gives slightly stronger guarantees, - `QScopedPointer<>` is better supported by Qt Creator's debugger UI and - therefore is preferred. + * Prefer using `std::unique_ptr<>` over `QScopedPointer<>` as it gives + stronger guarantees. Earlier revisions of this text recommended using + `QScopedPointer<>` because Qt Creator's debugger UI had a display helper + for it; it now has helpers for both. * Use `QVector` instead of `QList` where possible - see the [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. @@ -228,17 +229,17 @@ an empty doc-comment line. For in-code comments, the advice is as follows: * Don't restate what's happening in the code unless it's not really obvious. We assume the readers to have at least some command of C++ and Qt. If your - code is not obvious, consider rewriting it for clarity. + code is not obvious, consider making it clearer itself before commenting. * Both C++ and Qt still come with their arcane features and dark corners, and we don't want to limit anybody who feels they have a case for - variable templates, raw literals, or use `qAsConst` to avoid container + variable templates, raw literals, or use `std::as_const` to avoid container detachment. Use your experience to figure what might be less well-known to readers and comment such cases (references to web pages, Quotient wiki etc. - are very much ok). + are very much ok, the previous bullet notwithstanding). * Make sure to document not so much "what" but more "why" certain code is done the way it is. In the worst case, the logic of the code can be - reverse-engineered; you rarely can reverse-engineer the line of reasoning and - the pitfalls avoided. + reverse-engineered; but you can almost never reverse-engineer the line of + reasoning and the pitfalls avoided. ### Automated tests @@ -301,15 +302,23 @@ can produce noticeable GUI freezing or stuttering. When you don't see a way to reduce algorithmic complexity, embed occasional `processEvents()` invocations in heavy loops (see `Connection::saveState()` to get the idea). -Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. Beware of premature optimization and have profiling data around before going into some hardcore optimization. - -Speaking of profiling logs (see README.md on how to turn them on) - in order -to reduce small timespan logging spam, there's a default limit of at least -200 microseconds to log most operations with the `PROFILER` -(aka `quotient.profile.debug`) logging category. You can override this -limit by passing the new value (in microseconds) in `PROFILER_LOG_USECS` -definition to the compiler. If you happen to need to change it in runtime, -let me (`@kitsune`) know. +Having said that, there's always a trade-off between various attributes; +in particular, readability and maintainability of the code is more important +than squeezing every bit out of that clumsy algorithm. Beware of premature +optimization and profile the code before before diving into hardcore tweaking +that might not give the benefits you think it would. + +Speaking of profiling logs (see README.md on how to turn them on) - if you +expect some code to take considerable (more than 10k "simple operations") time +you might want to setup a `QElapsedTimer` and drop the elapsed time into logs +under `PROFILER` logging category (see the existing code for examples - +`room.cpp` has quite a few). In order to reduce small timespan logging spam, +`PROFILER` log lines are usually guarded by a check that the timer counted +some considerable time (200 microseconds by default, 20 microseconds for +tighter parts). It's possible to override this limit library-wide by passing +the new value (in microseconds) in `PROFILER_LOG_USECS` definition to +the compiler; I don't think anybody ever used this facility. If you used it, +and are reading this text - let me (`@kitsune`) know. ### Generated C++ code for CS API The code in `lib/csapi`, `lib/identity` and `lib/application-service`, although @@ -317,15 +326,9 @@ it resides in Git, is actually generated from the official Swagger/OpenAPI definition files. If you're unhappy with something in there and want to improve the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling -the below text can be found in .travis.yml (our Travis CI configuration -actually regenerates those files upon every build). As described below, -there is a handy build target for CMake; patches with a similar target -for qmake are (you guessed it) very welcome. -BACKUP - You can edit C++ files in these directories by hand but be -aware that any `make update-api` invocation will overwrite your changes. -If you also happen to change the API definition files, you should place the new -revision of the generated C++ files in a separate commit. +the below text can be found in .travis.yml (our CI configuration actually +regenerates those files upon every build). As described below, there is also +a handy build target for CMake. #### Why generate the code at all? Because otherwise we have to do monkey business of writing boilerplate code, @@ -414,31 +417,37 @@ The following warnings configuration is applied with GCC and Clang when using CM (the last one is to mute a warning triggered by Qt code for debug logging). We don't turn most of the warnings to errors but please treat them as such. If you use Qt Creator, the following line can be used with the Clang code model: -`-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables -Wno-unknown-attributes -Wno-comma`. +`-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables -Wno-unknown-attributes -Wno-comma -Wno-string-conversion -Wno-return-std-move-in-c++11`. ### Continuous Integration -We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. If your PR fails on any platform double-check that it's not your code causing it - and fix it if it is. +We use CI to check buildability and smoke-testing on Linux (GCC, Clang), +MacOS (Clang), and Windows (MSVC). Every PR will go through these, and you'll +see the traffic lights from them on the PR page. If your PR fails +on any platform double-check that it's not your code causing it - and fix +(or ask how to fix if you don't know) if it is. ### clang-format We strongly recommend using clang-format or, even better, use an IDE that -supports it. This will lay a tedious task of following the assumed -code style from your shoulders over to your computer. +supports it. This will lay over a tedious task of following the assumed +code style from your shoulders (and fingers) to your computer. ### Other tools Recent versions of Qt Creator and CLion can automatically run your code through -clang-tidy. The following list of clang-tidy checks slows Qt Creator analysis -quite considerably but gives a good insight without too many false positives: -`-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-move-forwarding-reference,bugprone-string-constructor,bugprone-undefined-memory-manipulation,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl03-c,cert-dcl21-cpp,cert-dcl50-cpp,cert-dcl54-cpp,cert-dcl58-cpp,cert-env33-c,cert-err09-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-err61-cpp,cert-fio38-c,cert-flp30-c,cert-msc30-c,cert-msc50-cpp,cert-oop11-cpp,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-slicing,hicpp-deprecated-headers,hicpp-invalid-access-moved,hicpp-member-init,hicpp-move-const-arg,hicpp-named-parameter,hicpp-new-delete-operators,hicpp-static-assert,hicpp-undelegated-constructor,hicpp-use-*,misc-misplaced-const,misc-new-delete-overloads,misc-non-copyable-objects,misc-redundant-expression,misc-static-assert,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,misc-unused-*,modernize-loop-convert,modernize-pass-by-value,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-*,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-*,performance-move-*,performance-type-promotion-in-math-fn,performance-unnecessary-*,readability-delete-null-pointer,readability-else-after-return,readability-inconsistent-declaration-parameter-name,readability-misleading-indentation,readability-redundant-*,readability-simplify-boolean-expr,readability-static-definition-in-anonymous-namespace,readability-uniqueptr-delete-release`. +clang-tidy. The following list of clang-tidy checks gives a good insight +without too many false positives: +`-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-constructor,bugprone-string-integer-assignment,bugprone-suspicious-*,bugprone-terminating-continue,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl03-c,cert-dcl21-cpp,cert-dcl50-cpp,cert-dcl54-cpp,cert-dcl58-cpp,cert-env33-c,cert-err09-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-err61-cpp,cert-fio38-c,cert-flp30-c,cert-msc30-c,cert-msc50-cpp,cert-oop11-cpp,clang-analyzer-apiModeling.StdCLibraryFunctions,clang-analyzer-core.CallAndMessage,clang-analyzer-core.NullDereference,clang-analyzer-cplusplus.*,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-non-private-member-variables-in-classes,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-slicing,hicpp-deprecated-headers,hicpp-invalid-access-moved,hicpp-member-init,hicpp-move-const-arg,hicpp-new-delete-operators,hicpp-static-assert,hicpp-undelegated-constructor,hicpp-use-*,misc-*,-misc-definitions-in-headers,-misc-no-recursion,-misc-non-private-member-variables-in-classes,modernize-loop-convert,modernize-pass-by-value,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-*,-modernize-use-trailing-return-type,performance-*,-performance-no-automatic-move,-performance-noexcept-move-constructor,-performance-unnecessary-*,readability-*,-readability-braces-around-statements,-readability-implicit-bool-conversion,-readability-isolate-declaration,-readability-magic-numbers,-readability-named-parameter,-readability-qualified-auto`. Qt Creator, in addition, knows about clazy, an even deeper Qt-aware static -analysis tool. Even level 1 clazy eats away CPU but produces some very relevant -notices that are easy to overlook otherwise, such as possible unintended copying -of a Qt container, or unguarded null pointers. You can use this time to time +analysis tool that produces some notices about Qt-specific issues that are +easy to overlook otherwise, such as possible unintended copying of +a Qt container, or unguarded null pointers. You can use this time to time (see Analyze menu in Qt Creator) instead of hogging your machine with -deep analysis as you type. +deep analysis as you type (or after each saving, depending on your version +of Qt Creator). Most of clazy checks are relevant to our code, except: +`fully-qualified-moc-types,overloaded-signal,qstring-comparison-to-implicit-char,foreach,non-pod-global-static,qstring-allocations,jni-signatures,qt4-qstring-from-array`. ### Submitting API changes -- 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 --- LICENSES/LGPL-2.1-or-later.txt | 462 +++++++++++++++++++++++++++++++++++ lib/avatar.cpp | 16 +- lib/avatar.h | 16 +- lib/connection.cpp | 16 +- lib/connection.h | 16 +- lib/connectiondata.cpp | 16 +- lib/connectiondata.h | 16 +- lib/converters.cpp | 16 +- lib/converters.h | 16 +- lib/eventitem.cpp | 16 +- lib/eventitem.h | 16 +- lib/events/accountdataevents.h | 16 +- lib/events/callanswerevent.cpp | 16 +- lib/events/callanswerevent.h | 16 +- lib/events/callcandidatesevent.cpp | 16 +- lib/events/callcandidatesevent.h | 16 +- lib/events/callhangupevent.cpp | 16 +- lib/events/callhangupevent.h | 16 +- lib/events/callinviteevent.cpp | 16 +- lib/events/callinviteevent.h | 16 +- lib/events/directchatevent.cpp | 16 +- lib/events/directchatevent.h | 16 +- lib/events/encryptionevent.h | 16 +- lib/events/event.cpp | 16 +- lib/events/event.h | 16 +- lib/events/eventcontent.cpp | 16 +- lib/events/eventcontent.h | 16 +- lib/events/eventloader.h | 16 +- lib/events/reactionevent.cpp | 16 +- lib/events/reactionevent.h | 16 +- lib/events/receiptevent.cpp | 16 +- lib/events/receiptevent.h | 16 +- lib/events/redactionevent.h | 16 +- lib/events/roomavatarevent.h | 16 +- lib/events/roomcanonicalaliasevent.h | 16 +- lib/events/roomcreateevent.cpp | 16 +- lib/events/roomcreateevent.h | 16 +- lib/events/roomevent.cpp | 16 +- lib/events/roomevent.h | 16 +- lib/events/roommemberevent.cpp | 16 +- lib/events/roommemberevent.h | 16 +- lib/events/roommessageevent.cpp | 16 +- lib/events/roommessageevent.h | 16 +- lib/events/roomtombstoneevent.cpp | 16 +- lib/events/roomtombstoneevent.h | 16 +- lib/events/simplestateevents.h | 16 +- lib/events/stateevent.cpp | 16 +- lib/events/stateevent.h | 16 +- lib/events/typingevent.cpp | 16 +- lib/events/typingevent.h | 16 +- lib/jobs/basejob.cpp | 16 +- lib/jobs/basejob.h | 16 +- lib/jobs/mediathumbnailjob.cpp | 16 +- lib/jobs/mediathumbnailjob.h | 16 +- lib/jobs/postreadmarkersjob.h | 16 +- lib/jobs/requestdata.h | 16 +- lib/jobs/syncjob.cpp | 16 +- lib/jobs/syncjob.h | 16 +- lib/joinstate.h | 16 +- lib/logging.cpp | 16 +- lib/logging.h | 16 +- lib/networkaccessmanager.cpp | 16 +- lib/networkaccessmanager.h | 16 +- lib/networksettings.cpp | 16 +- lib/networksettings.h | 16 +- lib/qt_connection_util.h | 16 +- lib/room.cpp | 16 +- lib/room.h | 16 +- lib/settings.h | 16 +- lib/syncdata.cpp | 16 +- lib/syncdata.h | 16 +- lib/user.cpp | 16 +- lib/user.h | 16 +- lib/util.cpp | 16 +- lib/util.h | 16 +- 75 files changed, 610 insertions(+), 1036 deletions(-) create mode 100644 LICENSES/LGPL-2.1-or-later.txt diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 00000000..aaaba168 --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,462 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as +the successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +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 Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/lib/avatar.cpp b/lib/avatar.cpp index c65aa25c..4548be02 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 "avatar.h" diff --git a/lib/avatar.h b/lib/avatar.h index 7a566bfa..111f565d 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/connection.cpp b/lib/connection.cpp index b76ca691..8f95f3a6 100644 --- a/lib/connection.cpp +++ b/lib/connection.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 "connection.h" diff --git a/lib/connection.h b/lib/connection.h index 07ae9f29..a32d0801 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -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 */ #pragma once diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index d57363d0..25ab775a 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.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 "connectiondata.h" diff --git a/lib/connectiondata.h b/lib/connectiondata.h index 000099d1..a3b2d39b 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -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 */ #pragma once diff --git a/lib/converters.cpp b/lib/converters.cpp index 9f570087..0df880a0 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "converters.h" diff --git a/lib/converters.h b/lib/converters.h index 81d7b6d8..d4f19b60 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 2e2b11c0..9c47e50d 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "eventitem.h" diff --git a/lib/eventitem.h b/lib/eventitem.h index 7b2c3c44..ae3d5762 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 0f240aa1..d0abf577 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index bf096534..f3d0a9a0 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 "callanswerevent.h" diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 2709882b..d7214468 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 */ #pragma once diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp index 24f0dd46..9b765064 100644 --- a/lib/events/callcandidatesevent.cpp +++ b/lib/events/callcandidatesevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 "callcandidatesevent.h" diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index e224f048..ae3bb150 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 */ #pragma once diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp index f2117f38..45b84cd4 100644 --- a/lib/events/callhangupevent.cpp +++ b/lib/events/callhangupevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 "callhangupevent.h" diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index 5d73fb62..432f72f5 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 */ #pragma once diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 63f331de..86478ada 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 "callinviteevent.h" diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index b067a492..304c89ac 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2017 Marius Gripsgard * - * 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 */ #pragma once diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index b4027e16..39d11072 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "directchatevent.h" diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index bb091c5c..373e36dc 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index cbb6d786..3431ddd8 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 96e33864..6014183e 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.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 "event.h" diff --git a/lib/events/event.h b/lib/events/event.h index 626a0229..e9d42333 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -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 */ #pragma once diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 802d8176..d7b109f7 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 "eventcontent.h" diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 0d4c047e..e0e4a5db 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index ebb96441..0d95daf5 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp index 003c8ead..9b43e372 100644 --- a/lib/events/reactionevent.cpp +++ b/lib/events/reactionevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 Kitsune Ral + * SPDX-FileCopyrightText: 2019 Kitsune Ral * - * 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 "reactionevent.h" diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 48b0bc6c..09166b24 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 Kitsune Ral + * SPDX-FileCopyrightText: 2019 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index deb3c4e8..b6f0fcdd 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach + * SPDX-FileCopyrightText: 2016 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 */ /* diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index b7adea44..ec297a6c 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach + * SPDX-FileCopyrightText: 2016 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 */ #pragma once diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index 3b3af18e..320db6f2 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index c2100eaa..649412e8 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index 4a21b7cc..eda94d2d 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2020 QMatrixClient project + * SPDX-FileCopyrightText: 2020 QMatrixClient project * - * 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 */ #pragma once diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index 0fc7d6b9..3d9ec4a3 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 QMatrixClient project + * SPDX-FileCopyrightText: 2019 QMatrixClient project * - * 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 "roomcreateevent.h" diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 91aefe9e..8328d38a 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 QMatrixClient project + * SPDX-FileCopyrightText: 2019 QMatrixClient project * - * 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 */ #pragma once diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index a2dbc07d..2b6ac2be 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "roomevent.h" diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 084cb524..3fafecfd 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index be47e412..6f5d5a52 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.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 "roommemberevent.h" diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index cebaaf10..b7a7c9df 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -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 */ #pragma once diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 616a034f..19d460b8 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.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 "roommessageevent.h" diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 2501d097..ebc9d564 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -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 */ #pragma once diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp index f93eb60d..163e1d3a 100644 --- a/lib/events/roomtombstoneevent.cpp +++ b/lib/events/roomtombstoneevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 QMatrixClient project + * SPDX-FileCopyrightText: 2019 QMatrixClient project * - * 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 "roomtombstoneevent.h" diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 2c2f0663..8d50aba0 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 QMatrixClient project + * SPDX-FileCopyrightText: 2019 QMatrixClient project * - * 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 */ #pragma once diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index cde5b0fd..58ba3b5a 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 5909e8a6..7bde12bb 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "stateevent.h" diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 20a85f83..0db37767 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp index e102fc79..7d3f71e5 100644 --- a/lib/events/typingevent.cpp +++ b/lib/events/typingevent.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 "typingevent.h" diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 97e1f9cc..8ca4f8e4 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -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 */ #pragma once diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 3fa1cd94..e6dc9f82 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.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 "basejob.h" diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index a0b89ef7..c2d42f49 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -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 */ #pragma once diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index a69f00e9..fbea8797 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach + * SPDX-FileCopyrightText: 2016 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 "mediathumbnailjob.h" diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index e6d39085..cb55a0b0 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach + * SPDX-FileCopyrightText: 2016 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 */ #pragma once diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h index 5a4d942c..ba965de9 100644 --- a/lib/jobs/postreadmarkersjob.h +++ b/lib/jobs/postreadmarkersjob.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 9cb5ecaf..2a227646 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 9087fe50..beb0a535 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach + * SPDX-FileCopyrightText: 2016 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 "syncjob.h" diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index bf139a7b..a7d10ed8 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach + * SPDX-FileCopyrightText: 2016 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 */ #pragma once diff --git a/lib/joinstate.h b/lib/joinstate.h index 31c2b6a7..1a7b1add 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -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 */ #pragma once diff --git a/lib/logging.cpp b/lib/logging.cpp index c346fbf1..c285821c 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Elvis Angelaccio + * SPDX-FileCopyrightText: 2017 Elvis Angelaccio * - * 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 "logging.h" diff --git a/lib/logging.h b/lib/logging.h index ce4131bb..77e0dad3 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index e8aa85df..43a8287a 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "networkaccessmanager.h" diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index a678b80f..6075767a 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 40ecba11..db16034a 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 "networksettings.h" diff --git a/lib/networksettings.h b/lib/networksettings.h index 2399cf5f..31602734 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2017 Kitsune Ral + * SPDX-FileCopyrightText: 2017 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 699735d4..158d7a40 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2019 Kitsune Ral + * SPDX-FileCopyrightText: 2019 Kitsune Ral * - * 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 */ #pragma once 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" diff --git a/lib/room.h b/lib/room.h index 7eee022c..c9205e9c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -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 */ #pragma once diff --git a/lib/settings.h b/lib/settings.h index c45764a6..badabec2 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Kitsune Ral + * SPDX-FileCopyrightText: 2016 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index e6472e18..f67ab6c7 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "syncdata.h" diff --git a/lib/syncdata.h b/lib/syncdata.h index 67d04557..d9868e46 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 */ #pragma once diff --git a/lib/user.cpp b/lib/user.cpp index c399ce88..9c2b76b5 100644 --- a/lib/user.cpp +++ b/lib/user.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 "user.h" diff --git a/lib/user.h b/lib/user.h index 19f57c30..d5c892ed 100644 --- a/lib/user.h +++ b/lib/user.h @@ -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 */ #pragma once diff --git a/lib/util.cpp b/lib/util.cpp index 36015d7a..14492ba6 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2018 Kitsune Ral + * SPDX-FileCopyrightText: 2018 Kitsune Ral * - * 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 "util.h" diff --git a/lib/util.h b/lib/util.h index 8c92df74..12e903ac 100644 --- a/lib/util.h +++ b/lib/util.h @@ -1,19 +1,7 @@ /****************************************************************************** - * Copyright (C) 2016 Kitsune Ral + * SPDX-FileCopyrightText: 2016 Kitsune Ral * - * 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 */ #pragma once -- cgit v1.2.3 From e731d9bbc36a628bd5a24fcf17efa2b47e4c2c1f Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 26 Dec 2020 17:31:08 +0100 Subject: Add a few more files --- lib/e2ee.h | 5 +++++ lib/encryptionmanager.cpp | 5 +++++ lib/encryptionmanager.h | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/lib/e2ee.h b/lib/e2ee.h index f49b9748..5f1857b6 100644 --- a/lib/e2ee.h +++ b/lib/e2ee.h @@ -1,3 +1,8 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "util.h" diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 4a1025b2..826656d3 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -1,3 +1,8 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #ifdef Quotient_E2EE_ENABLED #include "encryptionmanager.h" diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 5df15e83..0f507337 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #ifdef Quotient_E2EE_ENABLED #pragma once -- cgit v1.2.3 From 78cfde52d8f3ff04a07031a87a0c7218a3b0079f Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 27 Dec 2020 18:07:30 +0100 Subject: more :) --- lib/events/encryptedevent.cpp | 4 ++++ lib/events/encryptedevent.h | 4 ++++ lib/events/redactionevent.cpp | 4 ++++ lib/quotient_common.h | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 117aae37..dc9eaf2d 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "encryptedevent.h" using namespace Quotient; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 235b2aa4..9de08b00 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "e2ee.h" diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp index bf467718..5889773c 100644 --- a/lib/events/redactionevent.cpp +++ b/lib/events/redactionevent.cpp @@ -1 +1,5 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// +// SPDX-License-Identifier: CC0-1.0 + #include "redactionevent.h" diff --git a/lib/quotient_common.h b/lib/quotient_common.h index bb05af05..e2384f12 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include -- cgit v1.2.3 From 9d854e778d8d6ef8e03e1ea74fe958675b24fd45 Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Sun, 27 Dec 2020 21:24:06 +0100 Subject: Fix use-after-free of QNetworkReply in BaseJob Usually QNetworkAccessManager expects the user to delete the replies, but when the QNetworkAccessManager itself is deleted it deletes all pending replies (https://code.woboq.org/qt5/qtbase/src/network/access/qnetworkaccessmanager.cpp.html#529). This can lead to use-after-free crashes when d->reply is accessed. By putting the reply into a QPointer the exiting if(d->reply) checks can work properly. --- lib/jobs/basejob.cpp | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 3fa1cd94..2ac942f5 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -76,15 +77,6 @@ QDebug BaseJob::Status::dumpToLog(QDebug dbg) const return dbg << ": " << message; } -struct NetworkReplyDeleter : public QScopedPointerDeleteLater { - static inline void cleanup(QNetworkReply* reply) - { - if (reply && reply->isRunning()) - reply->abort(); - QScopedPointerDeleteLater::cleanup(reply); - } -}; - template constexpr auto make_array(Ts&&... items) { @@ -112,6 +104,16 @@ public: retryTimer.setSingleShot(true); } + ~Private() + { + if (reply) { + if (reply->isRunning()) { + reply->abort(); + } + delete reply; + } + } + void sendRequest(); /*! \brief Parse the response byte array into JSON * @@ -140,7 +142,10 @@ public: QByteArrayList expectedKeys; - QScopedPointer reply; + // When the QNetworkAccessManager is destroyed it destroys all pending replies. + // Using QPointer allows us to know when that happend. + QPointer reply; + Status status = Unprepared; QByteArray rawResponse; /// Contains a null document in case of non-JSON body (for a successful @@ -315,16 +320,16 @@ void BaseJob::Private::sendRequest() switch (verb) { case HttpVerb::Get: - reply.reset(connection->nam()->get(req)); + reply = connection->nam()->get(req); break; case HttpVerb::Post: - reply.reset(connection->nam()->post(req, requestData.source())); + reply = connection->nam()->post(req, requestData.source()); break; case HttpVerb::Put: - reply.reset(connection->nam()->put(req, requestData.source())); + reply = connection->nam()->put(req, requestData.source()); break; case HttpVerb::Delete: - reply.reset(connection->nam()->sendCustomRequest(req, "DELETE", requestData.source())); + reply = connection->nam()->sendCustomRequest(req, "DELETE", requestData.source()); break; } } -- cgit v1.2.3 From 2b52afbbf4316ec37eb6d1166990fa7ec5a61320 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 18:10:07 +0100 Subject: function_traits<>: define as empty by default An incomplete type was preventing some SFINAE cases for literal types. --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index 8c92df74..34f8714a 100644 --- a/lib/util.h +++ b/lib/util.h @@ -148,7 +148,7 @@ public: namespace _impl { template - struct fn_traits; + struct fn_traits {}; } /// Determine traits of an arbitrary function/lambda/functor -- cgit v1.2.3 From 639f1d482633a7adb72164c56e3b5ea429db96a5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 23:19:12 +0100 Subject: event.h: Minor tweaks around visit<> --- lib/events/event.h | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 626a0229..309ebddf 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -298,7 +298,7 @@ using Events = EventsArray; // === is<>(), eventCast<>() and visit<>() === -template +template inline bool is(const Event& e) { return e.type() == typeId(); @@ -309,7 +309,7 @@ inline bool isUnknown(const Event& e) return e.type() == unknownEventTypeId(); } -template +template inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast(&*eptr)) { @@ -319,7 +319,7 @@ inline auto eventCast(const BasePtrT& eptr) } // A single generic catch-all visitor -template +template inline auto visit(const BaseEventT& event, FnT&& visitor) -> decltype(visitor(event)) { @@ -327,18 +327,17 @@ inline auto visit(const BaseEventT& event, FnT&& visitor) } namespace _impl { - template - constexpr auto needs_downcast() - { - return !std::is_convertible_v>; - } + template + inline constexpr auto needs_downcast = + std::is_base_of_v>> + && !std::is_same_v>>; } // A single type-specific void visitor -template -inline std::enable_if_t<_impl::needs_downcast() +template +inline auto visit(const BaseT& event, FnT&& visitor) + -> std::enable_if_t<_impl::needs_downcast && std::is_void_v>> -visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t; if (is>(event)) @@ -347,10 +346,10 @@ visit(const BaseEventT& event, FnT&& visitor) // A single type-specific non-void visitor with an optional default value // non-voidness is guarded by defaultValue type -template -inline std::enable_if_t<_impl::needs_downcast(), fn_return_t> -visit(const BaseEventT& event, FnT&& visitor, - fn_return_t&& defaultValue = {}) +template +inline auto visit(const BaseT& event, FnT&& visitor, + fn_return_t&& defaultValue = {}) + -> std::enable_if_t<_impl::needs_downcast, fn_return_t> { using event_type = fn_arg_t; if (is>(event)) @@ -359,9 +358,10 @@ visit(const BaseEventT& event, FnT&& visitor, } // A chain of 2 or more visitors -template -inline fn_return_t visit(const BaseEventT& event, FnT1&& visitor1, - FnT2&& visitor2, FnTs&&... visitors) +template +inline std::common_type_t, fn_return_t> visit( + const BaseT& event, FnT1&& visitor1, FnT2&& visitor2, + FnTs&&... visitors) { using event_type1 = fn_arg_t; if (is>(event)) @@ -374,8 +374,8 @@ inline fn_return_t visit(const BaseEventT& event, FnT1&& visitor1, // over a range of event pointers template inline auto visitEach(RangeT&& events, FnTs&&... visitors) - -> std::enable_if_t, Event>> + -> std::enable_if_t(visitors)...))>> { for (auto&& evtPtr: events) visit(*evtPtr, std::forward(visitors)...); -- cgit v1.2.3 From 7c29f33121f58a52f43fa83183eaca47fa374980 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 18:33:51 +0100 Subject: More comments/documentation Notably, recommend using loginFlowsChanged() rather than homeserverChanged() to detect when a Connection object is ready for a login sequence. Related: #427. --- lib/connection.cpp | 5 ++--- lib/connection.h | 30 ++++++++++++++++++++++++++---- lib/events/event.cpp | 3 +++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b76ca691..694e4f16 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -345,11 +345,10 @@ void Connection::loginWithToken(const QByteArray& loginToken, loginToken, deviceId, initialDeviceName); } -void Connection::assumeIdentity(const QString& userId, - const QString& accessToken, +void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId) { - checkAndConnect(userId, + checkAndConnect(mxId, [=] { d->assumeIdentity(userId, accessToken, deviceId); }); } diff --git a/lib/connection.h b/lib/connection.h index 07ae9f29..2f638448 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -504,13 +504,35 @@ public Q_SLOTS: /** Determine and set the homeserver from MXID */ void resolveServer(const QString& mxid); + /** \brief Log in using a username and password pair + * + * Before logging in, this method checks if the homeserver is valid and + * supports the password login flow. If the homeserver is invalid but + * a full user MXID is provided, this method calls resolveServer() using + * this MXID. + * + * \sa resolveServer, resolveError, loginError + */ void loginWithPassword(const QString& userId, const QString& password, const QString& initialDeviceName, const QString& deviceId = {}); + /** \brief Log in using a login token + * + * One usual case for this method is the final stage of logging in via SSO. + * Unlike loginWithPassword() and assumeIdentity(), this method cannot + * resolve the server from the user name because the full user MXID is + * encoded in the login token. Callers should ensure the homeserver + * sanity in advance. + */ void loginWithToken(const QByteArray& loginToken, const QString& initialDeviceName, const QString& deviceId = {}); - void assumeIdentity(const QString& userId, const QString& accessToken, + /** \brief Use an existing access token to connect to the homeserver + * + * Similar to loginWithPassword(), this method checks that the homeserver + * URL is valid and tries to resolve it from the MXID in case it is not. + */ + void assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId); /*! \deprecated Use loginWithPassword instead */ void connectToServer(const QString& userId, const QString& password, @@ -662,9 +684,9 @@ Q_SIGNALS: * This was a signal resulting from a successful resolveServer(). * Since Connection now provides setHomeserver(), the HS URL * may change even without resolveServer() invocation. Use - * homeserverChanged() instead of resolved(). You can also use - * connectToServer and connectWithToken without the HS URL set in - * advance (i.e. without calling resolveServer), as they now trigger + * loginFLowsChanged() instead of resolved(). You can also use + * loginWith*() and assumeIdentity() without the HS URL set in + * advance (i.e. without calling resolveServer), as they trigger * server name resolution from MXID if the server URL is not valid. */ void resolved(); diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 96e33864..7b34114d 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -61,11 +61,14 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); } +// On const below: this is to catch accidental attempts to change event JSON +// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::contentJson() const { return fullJson()[ContentKeyL].toObject(); } +// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); -- cgit v1.2.3 From 6c9ff40dbd91cc4966f0ecf9ed817efc2495a2fb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 21:13:13 +0100 Subject: Connection: refactor the resolve/login code 1. resolveServer() now emits homeserverChanged() even when there's no .well-known file found. 2. checkAndConnect() entirely removed from the header file. 3. Sunny-day scenario for assumeIdentity() is now asynchronous, triggering a call to /whoami to double-check the user. 4. LoginFlow aliases is moved out from LoginFlows to Quotient namespace. --- lib/connection.cpp | 109 ++++++++++++++++++++++++++++++++++++++--------------- lib/connection.h | 36 ++++++------------ 2 files changed, 90 insertions(+), 55 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 694e4f16..f59d2962 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -37,6 +37,7 @@ #include "csapi/versions.h" #include "csapi/voip.h" #include "csapi/wellknown.h" +#include "csapi/whoami.h" #include "events/directchatevent.h" #include "events/eventloader.h" @@ -133,10 +134,27 @@ public: != "json"; bool lazyLoading = false; + /** \brief Check the homeserver and resolve it if needed, before connecting + * + * A single entry for functions that need to check whether the homeserver + * is valid before running. May execute connectFn either synchronously + * or asynchronously. In case of errors, emits resolveError() if + * the homeserver URL is not valid and cannot be resolved from userId, or + * the homeserver doesn't support the requested login flow. + * + * \param userId fully-qualified MXID to resolve HS from + * \param connectFn a function to execute once the HS URL is good + * \param flow optionally, a login flow that should be supported for + * connectFn to work; `none`, if there's no login flow + * requirements + * \sa resolveServer, resolveError + */ + void checkAndConnect(const QString &userId, + const std::function &connectFn, + const std::optional &flow = none); template void loginToServer(LoginArgTs&&... loginArgs); - void assumeIdentity(const QString& userId, const QString& accessToken, - const QString& deviceId); + void completeSetup(const QString &mxId); void removeRoom(const QString& roomId); void consumeRoomData(SyncDataList&& roomDataList, bool fromCache); @@ -264,12 +282,15 @@ void Connection::resolveServer(const QString& mxid) return; } - auto domain = maybeBaseUrl.host(); - qCDebug(MAIN) << "Finding the server" << domain; + qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host(); - d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file + const auto& oldBaseUrl = d->data->baseUrl(); + d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call d->resolverJob = callApi(); - connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl] { + connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl, oldBaseUrl] { + // Revert baseUrl so that setHomeserver() below triggers signals + // in case the base URL actually changed + d->data->setBaseUrl(oldBaseUrl); if (d->resolverJob->error() != BaseJob::NotFoundError) { if (!d->resolverJob->status().good()) { qCWarning(MAIN) @@ -297,6 +318,7 @@ void Connection::resolveServer(const QString& mxid) << "for base URL"; setHomeserver(maybeBaseUrl); } + Q_ASSERT(d->loginFlowsJob != nullptr); connect(d->loginFlowsJob, &BaseJob::success, this, &Connection::resolved); connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { @@ -324,10 +346,10 @@ void Connection::loginWithPassword(const QString& userId, const QString& initialDeviceName, const QString& deviceId) { - checkAndConnect(userId, [=] { + d->checkAndConnect(userId, [=] { d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId), password, /*token*/ "", deviceId, initialDeviceName); - }); + }, LoginFlows::Password); } SsoSession* Connection::prepareForSso(const QString& initialDeviceName, @@ -340,6 +362,7 @@ void Connection::loginWithToken(const QByteArray& loginToken, const QString& initialDeviceName, const QString& deviceId) { + Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token)); d->loginToServer(LoginFlows::Token.type, none /*user is encoded in loginToken*/, "" /*password*/, loginToken, deviceId, initialDeviceName); @@ -348,8 +371,21 @@ void Connection::loginWithToken(const QByteArray& loginToken, void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId) { - checkAndConnect(mxId, - [=] { d->assumeIdentity(userId, accessToken, deviceId); }); + d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] { + d->data->setToken(accessToken.toLatin1()); + d->data->setDeviceId(deviceId); // Can't we deduce this from access_token? + auto* job = callApi(); + connect(job, &BaseJob::success, this, [this, job, mxId] { + if (mxId != job->userId()) + qCWarning(MAIN).nospace() + << "The access_token owner (" << job->userId() + << ") is different from passed MXID (" << mxId << ")!"; + d->completeSetup(job->userId()); + }); + connect(job, &BaseJob::failure, this, [this, job] { + emit loginError(job->errorString(), job->rawDataSample()); + }); + }); } void Connection::reloadCapabilities() @@ -389,8 +425,9 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) auto loginJob = q->callApi(std::forward(loginArgs)...); connect(loginJob, &BaseJob::success, q, [this, loginJob] { - assumeIdentity(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); + data->setToken(loginJob->accessToken().toLatin1()); + data->setDeviceId(loginJob->deviceId()); + completeSetup(loginJob->userId()); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -403,17 +440,14 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) }); } -void Connection::Private::assumeIdentity(const QString& userId, - const QString& accessToken, - const QString& deviceId) +void Connection::Private::completeSetup(const QString& mxId) { - data->setUserId(userId); + data->setUserId(mxId); q->user(); // Creates a User object for the local user - data->setToken(accessToken.toLatin1()); - data->setDeviceId(deviceId); - q->setObjectName(userId % '/' % deviceId); + q->setObjectName(data->userId() % '/' % data->deviceId()); qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() - << "by user" << userId << "from device" << deviceId; + << "by user" << data->userId() + << "from device" << data->deviceId(); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -430,22 +464,37 @@ void Connection::Private::assumeIdentity(const QString& userId, q->reloadCapabilities(); } -void Connection::checkAndConnect(const QString& userId, - std::function connectFn) +void Connection::Private::checkAndConnect(const QString& userId, + const std::function& connectFn, + const std::optional& flow) { - if (d->data->baseUrl().isValid()) { + if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) { connectFn(); return; } - // Not good to go, try to fix the homeserver URL. + // Not good to go, try to ascertain the homeserver URL and flows if (userId.startsWith('@') && userId.indexOf(':') != -1) { - connectSingleShot(this, &Connection::homeserverChanged, this, connectFn); - // NB: doResolveServer can emit resolveError, so this is a part of - // checkAndConnect function contract. - resolveServer(userId); + q->resolveServer(userId); + if (flow) + connectSingleShot(q, &Connection::loginFlowsChanged, q, + [this, flow, connectFn] { + if (loginFlows.contains(*flow)) + connectFn(); + else + emit q->loginError( + tr("The homeserver at %1 does not support" + " the login flow '%2'") + .arg(data->baseUrl().toDisplayString()), + flow->type); + }); + else + connectSingleShot(q, &Connection::homeserverChanged, q, connectFn); } else - emit resolveError(tr("%1 is an invalid homeserver URL") - .arg(d->data->baseUrl().toString())); + emit q->resolveError(tr("Please provide the fully-qualified user id" + " (such as @user:example.org) so that the" + " homeserver could be resolved; the current" + " homeserver URL(%1) is not good") + .arg(data->baseUrl().toDisplayString())); } void Connection::logout() diff --git a/lib/connection.h b/lib/connection.h index 2f638448..dbe179e8 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -62,28 +62,27 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; +using LoginFlow = GetLoginFlowsJob::LoginFlow; + +/// Predefined login flows +struct LoginFlows { + static inline const LoginFlow Password { "m.login.password" }; + static inline const LoginFlow SSO { "m.login.sso" }; + static inline const LoginFlow Token { "m.login.token" }; +}; + // To simplify comparisons of LoginFlows -inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) +inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs) { return lhs.type == rhs.type; } -inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) +inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs) { return !(lhs == rhs); } -/// Predefined login flows -struct LoginFlows { - using LoginFlow = GetLoginFlowsJob::LoginFlow; - static inline const LoginFlow Password { "m.login.password" }; - static inline const LoginFlow SSO { "m.login.sso" }; - static inline const LoginFlow Token { "m.login.token" }; -}; - class Connection; using room_factory_t = @@ -882,19 +881,6 @@ private: class Private; QScopedPointer d; - /** - * A single entry for functions that need to check whether the - * homeserver is valid before running. May either execute connectFn - * synchronously or asynchronously (if tryResolve is true and - * a DNS lookup is initiated); in case of errors, emits resolveError - * if the homeserver URL is not valid and cannot be resolved from - * userId. - * - * @param userId - fully-qualified MXID to resolve HS from - * @param connectFn - a function to execute once the HS URL is good - */ - void checkAndConnect(const QString& userId, std::function connectFn); - static room_factory_t _roomFactory; static user_factory_t _userFactory; }; -- 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/jobs/basejob.cpp | 2 +- lib/jobs/basejob.h | 2 +- lib/room.cpp | 33 ++++++++++++--------------------- tests/quotest.cpp | 8 ++++---- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 2ac942f5..1a41f795 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -263,7 +263,7 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) d->expectedContentTypes = contentTypes; } -const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } +QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index a0b89ef7..a72f6120 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -363,7 +363,7 @@ protected: const QByteArrayList& expectedContentTypes() const; void addExpectedContentType(const QByteArray& contentType); void setExpectedContentTypes(const QByteArrayList& contentTypes); - const QByteArrayList expectedKeys() const; + QByteArrayList expectedKeys() const; void addExpectedKey(const QByteArray &key); void setExpectedKeys(const QByteArrayList &keys); 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); } diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 2b1f0229..a0bad753 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -186,7 +186,7 @@ TestManager::TestManager(int& argc, char** argv) connect(c, &Connection::connected, this, &TestManager::setupAndRun); connect(c, &Connection::resolveError, this, - [this](const QString& error) { + [](const QString& error) { clog << "Failed to resolve the server: " << error.toStdString() << endl; QCoreApplication::exit(-2); @@ -268,7 +268,7 @@ void TestManager::onNewRoom(Room* r) << endl; connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { clog << timeline.size() << " new event(s) in room " - << r->canonicalAlias().toStdString() << endl; + << r->objectName().toStdString() << endl; }); } @@ -319,13 +319,13 @@ TEST_IMPL(loadMembers) // It's not exactly correct because an arbitrary server might not support // lazy loading; but in the absence of capabilities framework we assume // it does. - if (r->memberNames().size() >= r->joinedCount()) { + if (r->users().size() >= r->joinedCount()) { clog << "Lazy loading doesn't seem to be enabled" << endl; FAIL_TEST(); } r->setDisplayed(); connect(r, &Room::allMembersLoaded, this, [this, thisTest, r] { - FINISH_TEST(r->memberNames().size() >= r->joinedCount()); + FINISH_TEST(r->users().size() >= r->joinedCount()); }); return false; } -- cgit v1.2.3 From 5d15e3b23649a54abdb3812c10f4a7d2ce07d7dd Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 23:32:18 +0100 Subject: BaseJob::initiate: add Q_LIKELY ...to show the sunny-day case. --- lib/jobs/basejob.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 1a41f795..d37f05bc 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -342,7 +342,7 @@ void BaseJob::beforeAbandon() { } void BaseJob::initiate(ConnectionData* connData, bool inBackground) { - if (connData && connData->baseUrl().isValid()) { + if (Q_LIKELY(connData && connData->baseUrl().isValid())) { d->inBackground = inBackground; d->connection = connData; doPrepare(); @@ -355,7 +355,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground) setStatus(FileError, "Request data not ready"); } Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this - if (status().code == Unprepared) { + if (Q_LIKELY(status().code == Unprepared)) { d->connection->submit(this); return; } -- 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/jobs/postreadmarkersjob.h | 38 -------------------------------------- lib/room.cpp | 6 +++--- libquotient.pri | 1 - 3 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 lib/jobs/postreadmarkersjob.h diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h deleted file mode 100644 index 5a4d942c..00000000 --- a/lib/jobs/postreadmarkersjob.h +++ /dev/null @@ -1,38 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral - * - * 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 - */ - -#pragma once - -#include "basejob.h" - -#include - -using namespace Quotient; - -class PostReadMarkersJob : public BaseJob { -public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob( - HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) - { - setRequestData( - QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); - } -}; 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; } diff --git a/libquotient.pri b/libquotient.pri index ef0f112a..677f60d3 100644 --- a/libquotient.pri +++ b/libquotient.pri @@ -72,7 +72,6 @@ HEADERS += \ $$SRCPATH/jobs/syncjob.h \ $$SRCPATH/jobs/mediathumbnailjob.h \ $$SRCPATH/jobs/downloadfilejob.h \ - $$SRCPATH/jobs/postreadmarkersjob.h \ $$files($$SRCPATH/csapi/*.h, false) \ $$files($$SRCPATH/csapi/definitions/*.h, false) \ $$files($$SRCPATH/csapi/definitions/wellknown/*.h, false) \ -- cgit v1.2.3 From 1b2b2ee36695d378a54753b7059acb4c063d7061 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 27 Dec 2020 23:14:17 +0100 Subject: Add a new logging category for room member changes MEMBERS, aka quotient.events.members.* - this was promised in ff020f3b but not actually done "before merging". --- lib/logging.cpp | 1 + lib/logging.h | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/logging.cpp b/lib/logging.cpp index c346fbf1..e6f8aa4b 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -24,6 +24,7 @@ LOGGING_CATEGORY(MAIN, "quotient.main") LOGGING_CATEGORY(EVENTS, "quotient.events") LOGGING_CATEGORY(STATE, "quotient.events.state") +LOGGING_CATEGORY(MEMBERS, "quotient.events.members") LOGGING_CATEGORY(MESSAGES, "quotient.events.messages") LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") LOGGING_CATEGORY(E2EE, "quotient.e2ee") diff --git a/lib/logging.h b/lib/logging.h index ce4131bb..05ca7988 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -23,6 +23,7 @@ Q_DECLARE_LOGGING_CATEGORY(MAIN) Q_DECLARE_LOGGING_CATEGORY(STATE) +Q_DECLARE_LOGGING_CATEGORY(MEMBERS) Q_DECLARE_LOGGING_CATEGORY(MESSAGES) Q_DECLARE_LOGGING_CATEGORY(EVENTS) Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) -- 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(-) 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(-) 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 e5bf7c2fa64716de0b75a67acc5f8620e8dc1704 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 28 Dec 2020 18:22:29 +0100 Subject: Add support for sticker events --- lib/events/stickerevent.cpp | 26 ++++++++++++++++++++++++++ lib/events/stickerevent.h | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 lib/events/stickerevent.cpp create mode 100644 lib/events/stickerevent.h diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp new file mode 100644 index 00000000..ea4dff3f --- /dev/null +++ b/lib/events/stickerevent.cpp @@ -0,0 +1,26 @@ +// SDPX-FileCopyrightText: 2020 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "stickerevent.h" + +using namespace Quotient; + +StickerEvent::StickerEvent(const QJsonObject &obj) + : RoomEvent(typeId(), obj) + , m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject())) +{} + +QString StickerEvent::body() const +{ + return content("body"_ls); +} + +const EventContent::ImageContent &StickerEvent::image() const +{ + return m_imageContent; +} + +QUrl StickerEvent::url() const +{ + return m_imageContent.url; +} diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h new file mode 100644 index 00000000..93671086 --- /dev/null +++ b/lib/events/stickerevent.h @@ -0,0 +1,38 @@ +// SDPX-FileCopyrightText: 2020 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" +#include "eventcontent.h" + +namespace Quotient { + +/// Sticker messages are specialised image messages that are displayed without +/// controls (e.g. no "download" link, or light-box view on click, as would be +/// displayed for for m.image events). +class StickerEvent : public RoomEvent +{ +public: + DEFINE_EVENT_TYPEID("m.sticker", StickerEvent) + + explicit StickerEvent(const QJsonObject &obj); + + /// \brief A textual representation or associated description of the + /// sticker image. + /// + /// This could be the alt text of the original image, or a message to + /// accompany and further describe the sticker. + QString body() const; + + /// \brief Metadata about the image referred to in url including a + /// thumbnail representation. + const EventContent::ImageContent &image() const; + + /// \brief The URL to the sticker image. This must be a valid mxc:// URI. + QUrl url() const; +private: + EventContent::ImageContent m_imageContent; +}; +REGISTER_EVENT_TYPE(StickerEvent) +} // namespace Quotient -- cgit v1.2.3 From 7366ea54135c46546253b72e2813d8d24f743481 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 28 Dec 2020 18:22:56 +0100 Subject: Add cmake change --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index eb3f9528..1b46f1a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -183,6 +183,7 @@ set(lib_SRCS lib/events/encryptionevent.cpp lib/events/encryptedevent.cpp lib/events/roomkeyevent.cpp + lib/events/stickerevent.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp lib/jobs/syncjob.cpp -- cgit v1.2.3 From 2174e1980fd2cf5407ba8cd7cabb85d74d242ed2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 28 Dec 2020 18:57:07 +0100 Subject: event.h: Fix breakage of AppVeyor CI The breakage was caused by 639f1d48. --- lib/events/event.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/events/event.h b/lib/events/event.h index 309ebddf..9f2f4f91 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -327,8 +327,9 @@ inline auto visit(const BaseEventT& event, FnT&& visitor) } namespace _impl { + // Using bool instead of auto below because auto apparently upsets MSVC template - inline constexpr auto needs_downcast = + inline constexpr bool needs_downcast = std::is_base_of_v>> && !std::is_same_v>>; } -- cgit v1.2.3 From 0f974c0f96f29035ee766e8913504fed4a4903a5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 28 Dec 2020 19:18:34 +0100 Subject: Connection: fix FTBFS with Quotient_E2EE_ENABLED --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index f59d2962..42b17570 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -451,7 +451,7 @@ void Connection::Private::completeSetup(const QString& mxId) #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED - AccountSettings accountSettings(userId); + AccountSettings accountSettings(data->userId()); encryptionManager.reset( new EncryptionManager(accountSettings.encryptionAccountPickle())); if (accountSettings.encryptionAccountPickle().isEmpty()) { -- cgit v1.2.3 From 68eabcc5e33f4f29f3fb2e57e3f1adbaf719661d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 29 Dec 2020 20:47:15 +0100 Subject: First shot at GHA --- .github/workflows/ci.yml | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..03909550 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CMake + +on: [push] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: RelWithDebInfo + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + max-parallel: 1 + matrix: + os: [ubuntu-18.04, macos-10.15] +# e2ee: [false, true] + compiler: [gcc, clang] + exclude: + - os: macos-10.15 + compiler: gcc + + steps: + - uses: actions/checkout@v2 + + - name: Cache Qt + id: cache-qt + uses: actions/cache@v2 + with: + path: ../Qt + key: ${{ runner.os }}-QtCache + + - name: Install Qt + uses: jurplel/install-qt-action@v2.11.1 + with: + version: '5.9.9' + cached: ${{ steps.cache-qt.outputs.cache-hit }} + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake + env: + CXX: ${{ matrix.compiler }} +# working-directory: ${{runner.workspace}}/build + # Note the current convention is to use the -S and -B options here to specify source + # and build directories, but this is only available with CMake 3.13 and higher. + # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 + run: cmake -Bbuild -S$GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + + - name: Build + run: cmake --build build --target quotest + +# - name: Test +# working-directory: ${{runner.workspace}}/build +# run: quotest ... -- cgit v1.2.3 From 2fed7f8aa2f86d80f406d01aafa0826c834d7ad3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Jan 2021 18:51:21 +0100 Subject: .clang-format: ensure older ClangFormat compatibility IndentGotoLabels is a ClangFormat 10 thing --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 7cc0f46e..02feaa84 100644 --- a/.clang-format +++ b/.clang-format @@ -92,7 +92,7 @@ IncludeCategories: #IncludeIsMainRegex: '(_test)?$' #IncludeIsMainSourceRegex: '' #IndentCaseLabels: false -IndentGotoLabels: false +#IndentGotoLabels: false IndentPPDirectives: AfterHash #IndentWidth: 4 #IndentWrappedFunctionNames: false -- cgit v1.2.3 From 23cf8bec21c8ea31be90822143db82a60b46e7bb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Jan 2021 20:17:33 +0100 Subject: .clang-format: more old ClangFormat compat --- .clang-format | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.clang-format b/.clang-format index 02feaa84..4df5ae84 100644 --- a/.clang-format +++ b/.clang-format @@ -92,7 +92,7 @@ IncludeCategories: #IncludeIsMainRegex: '(_test)?$' #IncludeIsMainSourceRegex: '' #IndentCaseLabels: false -#IndentGotoLabels: false +#IndentGotoLabels: false # Uncomment once on ClangFormat 10 IndentPPDirectives: AfterHash #IndentWidth: 4 #IndentWrappedFunctionNames: false @@ -122,7 +122,7 @@ SortUsingDeclarations: false #SpaceBeforeInheritanceColon: true #SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyBlock: false +#SpaceInEmptyBlock: false # Uncomment once on ClangFormat 10 #SpaceInEmptyParentheses: false #SpacesBeforeTrailingComments: 1 #SpacesInAngles: false -- cgit v1.2.3 From 1156f7bda0bb3728fb275cb1a12580bfb84156b1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 29 Dec 2020 21:08:59 +0100 Subject: Fix using a C compiler for CXX; don't fail-fast --- .github/workflows/ci.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03909550..af8b7eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,14 +14,15 @@ jobs: build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false max-parallel: 1 matrix: os: [ubuntu-18.04, macos-10.15] # e2ee: [false, true] - compiler: [gcc, clang] + compilers: [ 'CC=gcc CXX=g++', 'CC=clang CXX=clang++'] exclude: - os: macos-10.15 - compiler: gcc + compilers: 'CC=gcc CXX=g++' steps: - uses: actions/checkout@v2 @@ -30,26 +31,23 @@ jobs: id: cache-qt uses: actions/cache@v2 with: - path: ../Qt + path: ../../Qt key: ${{ runner.os }}-QtCache - name: Install Qt uses: jurplel/install-qt-action@v2.11.1 with: version: '5.9.9' + dir: ${{runner.workspace}}/.. cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Create Build Environment run: cmake -E make_directory ${{runner.workspace}}/build - name: Configure CMake - env: - CXX: ${{ matrix.compiler }} -# working-directory: ${{runner.workspace}}/build - # Note the current convention is to use the -S and -B options here to specify source - # and build directories, but this is only available with CMake 3.13 and higher. - # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 - run: cmake -Bbuild -S$GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + run: | + export ${{matrix.compilers}} + cmake -Bbuild -S$GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS - name: Build run: cmake --build build --target quotest -- cgit v1.2.3 From 0c375fcd31448ef470e9a840fb1130775ef05e88 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 30 Dec 2020 09:23:32 +0100 Subject: Use the default path to install Qt --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af8b7eb1..8d45b09a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,14 +31,14 @@ jobs: id: cache-qt uses: actions/cache@v2 with: - path: ../../Qt + path: ${{runner.workspace}}/Qt key: ${{ runner.os }}-QtCache - name: Install Qt uses: jurplel/install-qt-action@v2.11.1 with: version: '5.9.9' - dir: ${{runner.workspace}}/.. +# dir: ${{runner.home}} cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Create Build Environment -- cgit v1.2.3 From c315330cbee1c0fbd10352e0a38b51874f649cd0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 30 Dec 2020 16:03:35 +0100 Subject: Add E2EE config; install libQuotient, build quotest --- .github/workflows/ci.yml | 48 +++++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d45b09a..68fca800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,6 @@ name: CMake on: [push] -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: RelWithDebInfo - defaults: run: shell: bash @@ -15,43 +11,61 @@ jobs: runs-on: ${{ matrix.os }} strategy: fail-fast: false - max-parallel: 1 +# max-parallel: 1 matrix: os: [ubuntu-18.04, macos-10.15] -# e2ee: [false, true] + e2ee: ['e2ee', ''] compilers: [ 'CC=gcc CXX=g++', 'CC=clang CXX=clang++'] exclude: - os: macos-10.15 compilers: 'CC=gcc CXX=g++' + env: + DESTDIR: ${{ github.workspace }} + CMAKE_ARGS: '-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=${{ github.workspace }}/usr' + steps: - uses: actions/checkout@v2 + with: + submodules: ${{ matrix.e2ee == 'e2ee' }} - name: Cache Qt id: cache-qt uses: actions/cache@v2 with: - path: ${{runner.workspace}}/Qt + path: ${{ runner.workspace }}/Qt key: ${{ runner.os }}-QtCache - name: Install Qt uses: jurplel/install-qt-action@v2.11.1 with: version: '5.9.9' -# dir: ${{runner.home}} cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Create Build Environment - run: cmake -E make_directory ${{runner.workspace}}/build + run: cmake -E make_directory ${{ runner.workspace }}/build - - name: Configure CMake + - name: Build and install olm + if: ${{ matrix.e2ee == 'e2ee' }} run: | - export ${{matrix.compilers}} - cmake -Bbuild -S$GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS + git clone https://gitlab.matrix.org/matrix-org/olm.git + pushd olm + cmake . -Bbuild $CMAKE_ARGS + cmake --build build --target install + popd - - name: Build - run: cmake --build build --target quotest + - name: Configure libQuotient + run: | + export ${{ matrix.compilers }} + cmake $GITHUB_WORKSPACE -Bbuild $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee == 'e2ee' }} + + - name: Build and install libQuotient + run: cmake --build build --target install -# - name: Test -# working-directory: ${{runner.workspace}}/build -# run: quotest ... + - name: Build tests + run: | + cmake tests -Bbuild-test $CMAKE_ARGS + cmake --build build-test --target all + +# - name: Run tests +# run: build-test/quotest -- cgit v1.2.3 From 99beeb1f4a51a7fad2d1620aa5c7851a1938554c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 30 Dec 2020 18:10:44 +0100 Subject: A few renames; trigger by PRs too; start quotest --- .github/workflows/ci.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68fca800..b2d2cab4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,20 @@ -name: CMake +name: CI -on: [push] +on: + push: + pull_request: + types: [opened, reopened] defaults: run: shell: bash jobs: - build: + CI: runs-on: ${{ matrix.os }} strategy: fail-fast: false -# max-parallel: 1 + max-parallel: 1 matrix: os: [ubuntu-18.04, macos-10.15] e2ee: ['e2ee', ''] @@ -67,5 +70,8 @@ jobs: cmake tests -Bbuild-test $CMAKE_ARGS cmake --build build-test --target all -# - name: Run tests -# run: build-test/quotest + - name: Run tests + env: + TEST_USER: ${{ secrets.TEST_USER }} + TEST_PWD: ${{ secrets.TEST_PWD }} + run: build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "CI job ${{ github.job }}" -- cgit v1.2.3 From d46f9498c1deef0a229de693b1b5954fa9fd5bf2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Jan 2021 12:13:13 +0100 Subject: Better quotest origin line; setup CC/CXX globally There's no "job number" anymore but a textual description of the job is even better. --- .github/workflows/ci.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2d2cab4..b1474133 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,11 @@ jobs: max-parallel: 1 matrix: os: [ubuntu-18.04, macos-10.15] - e2ee: ['e2ee', ''] - compilers: [ 'CC=gcc CXX=g++', 'CC=clang CXX=clang++'] + e2ee: [e2ee, ''] + compiler: [ GCC, Clang ] exclude: - os: macos-10.15 - compilers: 'CC=gcc CXX=g++' + compiler: GCC env: DESTDIR: ${{ github.workspace }} @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: ${{ matrix.e2ee == 'e2ee' }} + submodules: ${{ matrix.e2ee != '' }} - name: Cache Qt id: cache-qt @@ -46,20 +46,29 @@ jobs: cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Create Build Environment - run: cmake -E make_directory ${{ runner.workspace }}/build + run: | + if [ "${{ matrix.compiler }}" == "GCC" ]; then + echo "CC=gcc" >>$GITHUB_ENV + echo "CXX=g++" >>$GITHUB_ENV + else + echo "CC=clang" >>$GITHUB_ENV + echo "CXX=clang++" >>$GITHUB_ENV + fi + echo "QUOTEST_ORIGIN=${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV + cmake -E make_directory ${{ runner.workspace }}/build - name: Build and install olm - if: ${{ matrix.e2ee == 'e2ee' }} + if: ${{ matrix.e2ee != '' }} run: | git clone https://gitlab.matrix.org/matrix-org/olm.git pushd olm cmake . -Bbuild $CMAKE_ARGS cmake --build build --target install popd + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN, E2EE" >>$GITHUB_ENV - name: Configure libQuotient run: | - export ${{ matrix.compilers }} cmake $GITHUB_WORKSPACE -Bbuild $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee == 'e2ee' }} - name: Build and install libQuotient @@ -74,4 +83,4 @@ jobs: env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - run: build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "CI job ${{ github.job }}" + run: build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" -- cgit v1.2.3 From 6a97ce7e39ff23a398dd5a53d76cfc9ea3d1efcb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Jan 2021 12:57:54 +0100 Subject: Add update-api support Along adding update-api support DESTDIR and CMAKE_ARGS were moved to the environment setup step in order to use `${{ runner.workspace }}` for installation and not pollute `${{ github.workspace }}` where libQuotient sources reside. --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1474133..4e4e9209 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,15 +17,15 @@ jobs: max-parallel: 1 matrix: os: [ubuntu-18.04, macos-10.15] - e2ee: [e2ee, ''] compiler: [ GCC, Clang ] + # Not using binary values here, to make the job captions more readable + e2ee: [ '', 'E2EE' ] + update-api: [ '', 'update-api' ] exclude: - os: macos-10.15 compiler: GCC - - env: - DESTDIR: ${{ github.workspace }} - CMAKE_ARGS: '-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=${{ github.workspace }}/usr' + - e2ee: '' # Somewhat reduce the number of combinations to check + update-api: 'update-api' steps: - uses: actions/checkout@v2 @@ -45,7 +45,7 @@ jobs: version: '5.9.9' cached: ${{ steps.cache-qt.outputs.cache-hit }} - - name: Create Build Environment + - name: Setup build environment run: | if [ "${{ matrix.compiler }}" == "GCC" ]; then echo "CC=gcc" >>$GITHUB_ENV @@ -55,21 +55,40 @@ jobs: echo "CXX=clang++" >>$GITHUB_ENV fi echo "QUOTEST_ORIGIN=${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV + echo "DESTDIR=${{ runner.workspace }}" >>$GITHUB_ENV + echo "CMAKE_ARGS=-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=${{ runner.workspace }}/usr" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build - name: Build and install olm - if: ${{ matrix.e2ee != '' }} + if: matrix.e2ee run: | + cd ${{ runner.workspace }} git clone https://gitlab.matrix.org/matrix-org/olm.git pushd olm - cmake . -Bbuild $CMAKE_ARGS + cmake . -B build $CMAKE_ARGS cmake --build build --target install popd - echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN, E2EE" >>$GITHUB_ENV + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - - name: Configure libQuotient + - name: Pull CS API and build GTAD + if: matrix.update-api run: | - cmake $GITHUB_WORKSPACE -Bbuild $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee == 'e2ee' }} + cd ${{ runner.workspace }} + git clone https://github.com/matrix-org/matrix-doc.git + git clone --recursive https://github.com/KitsuneRal/gtad.git + pushd gtad + cmake . $CMAKE_ARGS + cmake --build . + popd + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc -DGTAD_PATH=${{ runner.workspace }}/gtad/gtad" >>$GITHUB_ENV + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV + + - name: Configure libQuotient + run: cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} + + - name: Regenerate API code + if: matrix.update-api + run: cmake --build build --target update-api - name: Build and install libQuotient run: cmake --build build --target install -- cgit v1.2.3 From 788db43da4c8cd609189b7c2c5b4358cb303492c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Jan 2021 18:43:02 +0100 Subject: GTAD requires GCC 8 at least --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e4e9209..fa28127e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,9 @@ jobs: - name: Setup build environment run: | if [ "${{ matrix.compiler }}" == "GCC" ]; then - echo "CC=gcc" >>$GITHUB_ENV - echo "CXX=g++" >>$GITHUB_ENV + if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-8'; fi + echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV + echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV else echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV -- cgit v1.2.3 From 10ba44adf1f782a62ff9d30b929554057c35b00f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Jan 2021 21:34:15 +0100 Subject: Add Valgrind on Linux --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa28127e..c5f02a03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,12 @@ jobs: version: '5.9.9' cached: ${{ steps.cache-qt.outputs.cache-hit }} + - name: Install Valgrind + if: contains(matrix.os, 'ubuntu') + run: | + sudo apt-get install valgrind + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=tests/.valgrind.supp" >>$GITHUB_ENV + - name: Setup build environment run: | if [ "${{ matrix.compiler }}" == "GCC" ]; then @@ -103,4 +109,4 @@ jobs: env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - run: build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + run: $VALGRIND build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" -- cgit v1.2.3 From 0966f4821b6e5460b62d9fe8be14066ed001c1c2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 2 Jan 2021 16:18:11 +0100 Subject: Drop .travis.yml [skip ci] --- .travis.yml | 116 ------------------------------------------------------------ 1 file changed, 116 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9982be30..00000000 --- a/.travis.yml +++ /dev/null @@ -1,116 +0,0 @@ -language: cpp -dist: bionic - -git: - depth: false - -addons: - apt: - packages: - - ninja-build - - qt5-default - - qtmultimedia5-dev - - valgrind - - g++-8 - -env: - global: - - DESTDIR="$TRAVIS_BUILD_DIR/install" - - CMAKE_ARGS="-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=$DESTDIR/usr" - - VALGRIND="valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=tests/.valgrind.supp $VALGRIND_OPTIONS" - -matrix: # TODO: consider parallel execution, this thing takes an hour to run - include: - - os: linux - compiler: gcc - - os: linux - compiler: clang - - os: osx - osx_image: xcode10.1 - env: [ 'E2EE=1', 'VALGRIND=' ] - addons: - homebrew: - update: true - packages: - - qt5 - before_cache: - - brew cleanup - cache: - directories: - - $HOME/Library/Caches/Homebrew - # Check a few more advanced configurations - - os: linux - compiler: gcc - env: [ E2EE=1, UPDATE_API=1 ] # Check UPDATE_API with one of fatter options - - os: linux - compiler: clang - env: [ E2EE=1 ] - - os: linux - compiler: gcc - env: [ E2EE=1 ] - -before_install: -- if [ -f "$(which ninja)" ]; then export CMAKE_ARGS="$CMAKE_ARGS -GNinja"; fi -- if [ "$TRAVIS_OS_NAME" = "osx" ]; then export PATH=/usr/local/opt/qt/bin:$PATH; fi -# The recent GTAD uses std::filesystem that's not available in stock bionic -- | - if [ -n "$UPDATE_API" ]; then - export CC=gcc-8 CXX=g++-8 - export CMAKE_UPDATE_API_ARGS="-DMATRIX_DOC_PATH=../matrix-doc -DGTAD_PATH=../gtad/gtad" - fi -- | - if [ -n "$E2EE" ]; then - export CMAKE_E2EE_ARGS="-DQuotient_ENABLE_E2EE=ON" - export QMAKE_E2EE_ARGS='"DEFINES += Quotient_E2EE_ENABLED USE_INTREE_LIBQOLM" "INCLUDEPATH += olm/include" "LIBS += -Lolm/build"' - export LIB_PATH_E2EE=olm/build - fi -# RPM spec-style: swallow a command with default parameters into an alias -# and add/override parameters further in the code if/as necessary -- shopt -s expand_aliases -- alias _cmake_config='cmake $CMAKE_ARGS . -Bbuild' -- alias _cmake_build='cmake --build build' - -install: -- pushd .. # Go out of libQuotient source tree -- | - if [ -n "$E2EE" ]; then - git clone https://gitlab.matrix.org/matrix-org/olm.git - pushd olm - _cmake_config - _cmake_build --target install - popd - fi - -- | - if [ -n "$UPDATE_API" ]; then - git clone https://github.com/matrix-org/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - pushd gtad - cmake $CMAKE_ARGS . - cmake --build . - popd - fi -- popd # back to libQuotient source tree - -before_script: -- _cmake_config $CMAKE_UPDATE_API_ARGS $CMAKE_E2EE_ARGS -- if [ -n "$UPDATE_API" ]; then _cmake_build --target update-api; fi - -script: -- _cmake_build --target install -# Build quotest with the installed libQuotient -- cmake $CMAKE_ARGS tests -Bbuild-test -- cmake --build build-test --target all -# Build with qmake -- qmake -Wall quotest.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" -- $QMAKE_E2EE_ARGS -- make all -# Run the qmake-compiled quotest under valgrind -- if [ "$TEST_USER" != "" ]; then LD_LIBRARY_PATH="$LIB_PATH_E2EE" $VALGRIND ./quotest "$TEST_USER" "$TEST_PWD" quotest-travis '#quotest:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi - -notifications: - webhooks: - urls: - - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGtpdHN1bmUlM0FtYXRyaXgub3JnLyUyMVBDelV0eHRPalV5U3hTZWxvZiUzQW1hdHJpeC5vcmc" - on_success: change # always|never|change - on_failure: always - on_start: never -- cgit v1.2.3 From b3e3bd7d6f6934257c17c486260ac4670373141c Mon Sep 17 00:00:00 2001 From: Heiko Becker Date: Wed, 6 Jan 2021 19:49:38 +0100 Subject: Use CMAKE_INSTALL_DATADIR instead of hard-coding share Signed-off-by: Heiko Becker --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b46f1a7..ce2463bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,7 +331,7 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}ConfigVersion.cmake" DESTINATION ${ConfigFilesLocation} ) -install(EXPORT_ANDROID_MK ${PROJECT_NAME}Targets DESTINATION share/ndk-modules) +install(EXPORT_ANDROID_MK ${PROJECT_NAME}Targets DESTINATION ${CMAKE_INSTALL_DATADIR}/ndk-modules) if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) -- cgit v1.2.3 From 8fc5de0529458851a4cd5c042b2b2f2543068c22 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 7 Jan 2021 20:16:59 +0100 Subject: Prefer connecting to BaseJob::result(), not finished() ...because finished() includes abandoning and should only be relevant when lifecycle issues are involved. (cherry picked from commit 90d41b697af39253483d9bcca4e57b11d2197112) --- lib/connection.cpp | 2 +- tests/quotest.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 42b17570..9afdfe7e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1555,7 +1555,7 @@ void Connection::setHomeserver(const QUrl& url) // Whenever a homeserver is updated, retrieve available login flows from it d->loginFlowsJob = callApi(BackgroundRequest); - connect(d->loginFlowsJob, &BaseJob::finished, this, [this] { + connect(d->loginFlowsJob, &BaseJob::result, this, [this] { if (d->loginFlowsJob->status().good()) d->loginFlows = d->loginFlowsJob->flows(); else diff --git a/tests/quotest.cpp b/tests/quotest.cpp index a0bad753..7ab7365f 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -828,7 +828,7 @@ void TestManager::conclude() // .then(this, &TestManager::finalize); // Qt-style or // .then([this] { finalize(); }); // STL-style auto* job = room->leaveRoom(); - connect(job, &BaseJob::finished, this, [this, job,plainReport] { + connect(job, &BaseJob::result, this, [this, job,plainReport] { Q_ASSERT(job->status().good()); finalize(); // Still flying, as the exit() connection in finalize() is queued -- cgit v1.2.3 From 32794f67621f87fa796c423a900385e6a1fba4b9 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 7 Jan 2021 21:15:28 +0100 Subject: BaseJob: more logging (cherry picked from commit 4f06d46d6d6062d6d17f69eeaddb7810edac5bbf) --- lib/jobs/basejob.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index d37f05bc..0c030d48 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -199,6 +199,7 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); connect(&d->retryTimer, &QTimer::timeout, this, [this] { + qCDebug(d->logCat) << "Retrying" << this; d->connection->submit(this); }); } @@ -374,8 +375,11 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground) void BaseJob::sendRequest() { - if (status().code == Abandoned) + if (status().code == Abandoned) { + qCDebug(d->logCat) << "Won't proceed with the abandoned request:" + << d->dumpRequest(); return; + } Q_ASSERT(d->connection && status().code == Pending); qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest(); d->needsToken |= d->connection->needsToken(objectName()); -- cgit v1.2.3 From 6af9ae29cb3c29e8e196d303409da369d23c3450 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 7 Jan 2021 21:22:55 +0100 Subject: Connection::resolveServer: abandon is not a failure So just reset the base URL and return, with no error signals. (cherry picked from commit be00308ad67286b45912202750fe49fb87f16e4a) --- lib/connection.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 9afdfe7e..b8b131bf 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -287,10 +287,14 @@ void Connection::resolveServer(const QString& mxid) const auto& oldBaseUrl = d->data->baseUrl(); d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call d->resolverJob = callApi(); + // Connect to finished() to make sure baseUrl is restored in any case connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl, oldBaseUrl] { // Revert baseUrl so that setHomeserver() below triggers signals // in case the base URL actually changed d->data->setBaseUrl(oldBaseUrl); + if (d->resolverJob->error() == BaseJob::Abandoned) + return; + if (d->resolverJob->error() != BaseJob::NotFoundError) { if (!d->resolverJob->status().good()) { qCWarning(MAIN) @@ -318,7 +322,7 @@ void Connection::resolveServer(const QString& mxid) << "for base URL"; setHomeserver(maybeBaseUrl); } - Q_ASSERT(d->loginFlowsJob != nullptr); + Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver() connect(d->loginFlowsJob, &BaseJob::success, this, &Connection::resolved); connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { -- cgit v1.2.3 From aa790406aa0b076938f877e38545baf481a986ec Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 7 Jan 2021 21:18:01 +0100 Subject: BaseJob: setStatus(Pending) on scheduling a retry Fixes #437. (cherry picked from commit 12e00b234e5c5f4ed57b5c400d06f780e71014f4) --- lib/jobs/basejob.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 0c030d48..8b4b33fe 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -625,6 +625,7 @@ void BaseJob::finishJob() qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken << " in " << retryIn.count() << " s"; + setStatus(Pending, "Pending retry"); d->retryTimer.start(retryIn); emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); return; -- 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/avatar.cpp | 14 +++++++------- lib/connection.cpp | 8 ++++---- lib/jobs/basejob.h | 2 +- lib/room.cpp | 14 +++++++------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/avatar.cpp b/lib/avatar.cpp index c65aa25c..df43f8b0 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -37,9 +37,9 @@ public: explicit Private(QUrl url = {}) : _url(move(url)) {} ~Private() { - if (isJobRunning(_thumbnailRequest)) + if (isJobPending(_thumbnailRequest)) _thumbnailRequest->abandon(); - if (isJobRunning(_uploadRequest)) + if (isJobPending(_uploadRequest)) _uploadRequest->abandon(); } @@ -87,7 +87,7 @@ QImage Avatar::get(Connection* connection, int width, int height, bool Avatar::upload(Connection* connection, const QString& fileName, upload_callback_t callback) const { - if (isJobRunning(d->_uploadRequest)) + if (isJobPending(d->_uploadRequest)) return false; return d->upload(connection->uploadFile(fileName), move(callback)); } @@ -95,7 +95,7 @@ bool Avatar::upload(Connection* connection, const QString& fileName, bool Avatar::upload(Connection* connection, QIODevice* source, upload_callback_t callback) const { - if (isJobRunning(d->_uploadRequest) || !source->isReadable()) + if (isJobPending(d->_uploadRequest) || !source->isReadable()) return false; return d->upload(connection->uploadContent(source), move(callback)); } @@ -125,7 +125,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size, && checkUrl(_url)) { qCDebug(MAIN) << "Getting avatar from" << _url.toString(); _requestedSize = size; - if (isJobRunning(_thumbnailRequest)) + if (isJobPending(_thumbnailRequest)) _thumbnailRequest->abandon(); if (callback) callbacks.emplace_back(move(callback)); @@ -157,7 +157,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size, bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback) { _uploadRequest = job; - if (!isJobRunning(_uploadRequest)) + if (!isJobPending(_uploadRequest)) return false; _uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest, [job, callback] { callback(job->contentUri()); }); @@ -194,7 +194,7 @@ bool Avatar::updateUrl(const QUrl& newUrl) d->_url = newUrl; d->_imageSource = Private::Unknown; - if (isJobRunning(d->_thumbnailRequest)) + if (isJobPending(d->_thumbnailRequest)) d->_thumbnailRequest->abandon(); return true; } diff --git a/lib/connection.cpp b/lib/connection.cpp index b8b131bf..b8294393 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -271,7 +271,7 @@ Connection::~Connection() void Connection::resolveServer(const QString& mxid) { - if (isJobRunning(d->resolverJob)) + if (isJobPending(d->resolverJob)) d->resolverJob->abandon(); auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid)); @@ -1229,7 +1229,7 @@ QByteArray Connection::accessToken() const { // The logout job needs access token to do its job; so the token is // kept inside d->data but no more exposed to the outside world. - return isJobRunning(d->logoutJob) ? QByteArray() : d->data->accessToken(); + return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken(); } bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } @@ -1544,10 +1544,10 @@ QByteArray Connection::generateTxnId() const void Connection::setHomeserver(const QUrl& url) { - if (isJobRunning(d->resolverJob)) + if (isJobPending(d->resolverJob)) d->resolverJob->abandon(); d->resolverJob = nullptr; - if (isJobRunning(d->loginFlowsJob)) + if (isJobPending(d->loginFlowsJob)) d->loginFlowsJob->abandon(); d->loginFlowsJob = nullptr; d->loginFlows.clear(); diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index a72f6120..317d5701 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -478,7 +478,7 @@ private: QScopedPointer d; }; -inline bool isJobRunning(BaseJob* job) +inline bool isJobPending(BaseJob* job) { return job && job->error() == BaseJob::Pending; } 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 6101971af86fdecd084759aa039b9d20a9d662a7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 7 Jan 2021 21:39:28 +0100 Subject: Connection: don't explicitly reset QPointers See #437 for the discussion. --- lib/connection.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b8294393..fce135ed 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1546,10 +1546,8 @@ void Connection::setHomeserver(const QUrl& url) { if (isJobPending(d->resolverJob)) d->resolverJob->abandon(); - d->resolverJob = nullptr; if (isJobPending(d->loginFlowsJob)) d->loginFlowsJob->abandon(); - d->loginFlowsJob = nullptr; d->loginFlows.clear(); if (homeserver() != url) { -- cgit v1.2.3 From 70846cf880c2b2e6dc9aa225aa3fb0e86ca04568 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 8 Jan 2021 07:41:37 +0100 Subject: quotest: use the target room for loadMembers test Now that we've crowded it with a few synthetic users, lazy-loading of members doesn't some other room to get tested. Bonus: Connection::roomByAlias() has its own very simple test now. (cherry picked from commit d09383d5dc7379c534860b5a66467a32d6adc932) --- tests/quotest.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 7ab7365f..98c01cfc 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -91,6 +91,7 @@ public slots: void doTest(const QByteArray& testName); private slots: + TEST_DECL(findRoomByAlias) TEST_DECL(loadMembers) TEST_DECL(sendMessage) TEST_DECL(sendReaction) @@ -306,26 +307,25 @@ void TestManager::doTests() }); } +TEST_IMPL(findRoomByAlias) +{ + auto* roomByAlias = connection()->roomByAlias(targetRoom->canonicalAlias(), + JoinState::Join); + FINISH_TEST(roomByAlias == targetRoom); +} + TEST_IMPL(loadMembers) { - // Trying to load members from another (larger) room - const auto& testRoomAlias = QStringLiteral("#test:matrix.org"); - auto* r = connection()->roomByAlias(testRoomAlias, JoinState::Join); - if (!r) { - clog << testRoomAlias.toStdString() - << " is not found in the test user's rooms" << endl; - FAIL_TEST(); - } // It's not exactly correct because an arbitrary server might not support // lazy loading; but in the absence of capabilities framework we assume // it does. - if (r->users().size() >= r->joinedCount()) { + if (targetRoom->users().size() >= targetRoom->joinedCount()) { clog << "Lazy loading doesn't seem to be enabled" << endl; FAIL_TEST(); } - r->setDisplayed(); - connect(r, &Room::allMembersLoaded, this, [this, thisTest, r] { - FINISH_TEST(r->users().size() >= r->joinedCount()); + targetRoom->setDisplayed(); + connect(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { + FINISH_TEST(targetRoom->users().size() >= targetRoom->joinedCount()); }); return false; } -- cgit v1.2.3 From f0e9534d1aff015d7d2822e58f615bd1434153c9 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 11 Jan 2021 11:42:54 +0100 Subject: LGTM: fine-tune the set of analysed files --- .lgtm.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.lgtm.yml b/.lgtm.yml index b9952f40..f6dfb229 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -1,3 +1,8 @@ +path_classifiers: + library: + - 3rdparty/* + test: + - exclude: tests/quotest.cpp # Let alerts from this come up too extraction: cpp: prepare: -- cgit v1.2.3 From 21ff5c1e92624b09a1f065af07d8330c8aedcd58 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 8 Jan 2021 08:37:38 +0100 Subject: Add a timeout to quotest runs The current Quotest gets stuck somewhere, and the its big internal 3-minute watchdog doesn't cut it for some reason. While investigating that, an external timeout would be quite handy. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5f02a03..a374fac0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,3 +110,4 @@ jobs: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} run: $VALGRIND build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From 723563bb27ceb17594eb04784449cd9f8ea6001d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 11 Jan 2021 19:15:36 +0100 Subject: Don't run the test if TEST_USER is empty --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a374fac0..082ce005 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,7 @@ jobs: cmake --build build-test --target all - name: Run tests + if: secrets.TEST_USER != '' env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} -- cgit v1.2.3 From 0990ab8121002e1613ac68558e07c74855370713 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 11 Jan 2021 19:17:43 +0100 Subject: Ok, do the same in a different way --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 082ce005..201a4186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,9 +106,9 @@ jobs: cmake --build build-test --target all - name: Run tests - if: secrets.TEST_USER != '' env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - run: $VALGRIND build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + run: | + [[ -z "$TEST_USER" ]] || $VALGRIND build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From 7ac0bf044e521be4043c0b545d42323e2f9101a4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 12 Jan 2021 19:26:34 +0100 Subject: EventItemBase: Allow adding custom data A new field of std::any type is added that allows clients to "annotate" any event item with arbitrary kind of data. This is mainly intended so that clients could calculate certain information about the item (e.g. special formatting depending on the event contents, or position) without having to calculate this information every time it is visualised. In case of Quaternion, the idea is to calculate the "spamminess" of the event basing on the past activity of a given user in this room - calculating it upon displaying each event is extremely heavyweight. --- lib/eventitem.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/eventitem.h b/lib/eventitem.h index 7b2c3c44..137ddf63 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -20,6 +20,7 @@ #include "events/stateevent.h" +#include #include namespace Quotient { @@ -72,6 +73,12 @@ public: return std::exchange(evt, move(other)); } + /// Store arbitrary data with the event item + void setUserData(std::any userData) { data = userData; } + /// Obtain custom data previously stored with the event item + const std::any& userdata() const { return data; } + std::any& userData() { return data; } + protected: template EventT* getAs() @@ -81,6 +88,7 @@ protected: private: RoomEventPtr evt; + std::any data; }; class TimelineItem : public EventItemBase { -- cgit v1.2.3 From b876fa4de3d7f58418bd03935f2336eb9edff3ac Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 14 Jan 2021 08:27:05 +0100 Subject: EncryptionManager: fix a typo --- lib/encryptionmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 4a1025b2..2f01c3e7 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -245,7 +245,7 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { d->uploadOneTimeKeysInitJob = connection->callApi(); connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); + d->setOneTimeKeyCounts(d->uploadOneTimeKeyInitJob->oneTimeKeyCounts()); }); } -- cgit v1.2.3 From d123822b2d0eb2854f1bcdb98c1cfa4f0257ed43 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 14 Jan 2021 08:40:55 +0100 Subject: Fix a typo in the previous typo fix --- lib/encryptionmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 2f01c3e7..e585fae8 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -245,7 +245,7 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { d->uploadOneTimeKeysInitJob = connection->callApi(); connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadOneTimeKeyInitJob->oneTimeKeyCounts()); + d->setOneTimeKeyCounts(d->uploadOneTimeKeysInitJob->oneTimeKeyCounts()); }); } -- cgit v1.2.3 From 7ce8513f65a21a02798447f745eb90290cb5a6fb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Jan 2021 07:59:55 +0100 Subject: Drop a file deleted in master --- lib/jobs/postreadmarkersjob.h | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 lib/jobs/postreadmarkersjob.h diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h deleted file mode 100644 index ba965de9..00000000 --- a/lib/jobs/postreadmarkersjob.h +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ - -#pragma once - -#include "basejob.h" - -#include - -using namespace Quotient; - -class PostReadMarkersJob : public BaseJob { -public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob( - HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) - { - setRequestData( - QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); - } -}; -- 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. --- .clang-format | 8 ++++---- CONTRIBUTING.md | 14 ++++++++++++-- gtad/data.h.mustache | 4 ++++ gtad/operation.cpp.mustache | 4 ++++ gtad/operation.h.mustache | 4 ++++ lib/avatar.cpp | 7 ++----- lib/avatar.h | 9 +++------ lib/connection.cpp | 10 +++++----- lib/connection.h | 9 ++++----- lib/connectiondata.cpp | 8 +++----- lib/connectiondata.h | 8 +++----- lib/converters.cpp | 7 ++----- lib/converters.h | 7 ++----- lib/e2ee.h | 1 - lib/encryptionmanager.cpp | 1 - lib/encryptionmanager.h | 1 - lib/eventitem.cpp | 7 ++----- lib/eventitem.h | 7 ++----- lib/events/accountdataevents.h | 7 ++----- lib/events/callanswerevent.cpp | 8 +++----- lib/events/callanswerevent.h | 8 +++----- lib/events/callcandidatesevent.cpp | 8 +++----- lib/events/callcandidatesevent.h | 9 ++++----- lib/events/callhangupevent.cpp | 1 + lib/events/callhangupevent.h | 8 +++----- lib/events/callinviteevent.cpp | 8 +++----- lib/events/callinviteevent.h | 8 +++----- lib/events/directchatevent.cpp | 7 ++----- lib/events/directchatevent.h | 7 ++----- lib/events/encryptedevent.cpp | 1 - lib/events/encryptedevent.h | 1 - lib/events/encryptionevent.cpp | 4 ++++ lib/events/encryptionevent.h | 8 +++----- lib/events/event.cpp | 7 ++----- lib/events/event.h | 7 ++----- lib/events/eventcontent.cpp | 7 ++----- lib/events/eventcontent.h | 7 ++----- lib/events/eventloader.h | 7 ++----- lib/events/reactionevent.cpp | 7 ++----- lib/events/reactionevent.h | 7 ++----- lib/events/receiptevent.cpp | 7 ++----- lib/events/receiptevent.h | 7 ++----- lib/events/redactionevent.cpp | 5 ----- lib/events/redactionevent.h | 7 ++----- lib/events/roomavatarevent.h | 7 ++----- lib/events/roomcanonicalaliasevent.h | 8 +++----- lib/events/roomcreateevent.cpp | 7 ++----- lib/events/roomcreateevent.h | 7 ++----- lib/events/roomevent.cpp | 7 ++----- lib/events/roomevent.h | 7 ++----- lib/events/roomkeyevent.cpp | 3 +++ lib/events/roomkeyevent.h | 3 +++ lib/events/roommemberevent.cpp | 8 +++----- lib/events/roommemberevent.h | 9 ++++----- lib/events/roommessageevent.cpp | 9 ++++----- lib/events/roommessageevent.h | 9 ++++----- lib/events/roompowerlevelsevent.cpp | 3 +++ lib/events/roompowerlevelsevent.h | 3 +++ lib/events/roomtombstoneevent.cpp | 7 ++----- lib/events/roomtombstoneevent.h | 7 ++----- lib/events/simplestateevents.h | 7 ++----- lib/events/stateevent.cpp | 7 ++----- lib/events/stateevent.h | 7 ++----- lib/events/typingevent.cpp | 7 ++----- lib/events/typingevent.h | 7 ++----- lib/jobs/basejob.cpp | 8 +++----- lib/jobs/basejob.h | 8 +++----- lib/jobs/downloadfilejob.cpp | 3 +++ lib/jobs/downloadfilejob.h | 3 +++ lib/jobs/mediathumbnailjob.cpp | 7 ++----- lib/jobs/mediathumbnailjob.h | 7 ++----- lib/jobs/requestdata.cpp | 3 +++ lib/jobs/requestdata.h | 7 ++----- lib/jobs/syncjob.cpp | 7 ++----- lib/jobs/syncjob.h | 7 ++----- lib/joinstate.h | 7 ++----- lib/logging.cpp | 8 +++----- lib/logging.h | 8 +++----- lib/networkaccessmanager.cpp | 7 ++----- lib/networkaccessmanager.h | 7 ++----- lib/networksettings.cpp | 7 ++----- lib/networksettings.h | 7 ++----- lib/qt_connection_util.h | 7 ++----- lib/quotient_common.h | 1 - lib/room.cpp | 13 ++++++++----- lib/room.h | 13 ++++++++----- lib/settings.cpp | 3 +++ lib/settings.h | 7 ++----- lib/ssosession.cpp | 3 +++ lib/ssosession.h | 3 +++ lib/syncdata.cpp | 7 ++----- lib/syncdata.h | 7 ++----- lib/uri.cpp | 3 +++ lib/uri.h | 3 +++ lib/uriresolver.cpp | 3 +++ lib/uriresolver.h | 3 +++ lib/user.cpp | 8 +++----- lib/user.h | 8 +++----- lib/util.cpp | 8 +++----- lib/util.h | 8 +++----- tests/quotest.cpp | 2 ++ 101 files changed, 266 insertions(+), 378 deletions(-) delete mode 100644 lib/events/redactionevent.cpp diff --git a/.clang-format b/.clang-format index 4df5ae84..4510d46e 100644 --- a/.clang-format +++ b/.clang-format @@ -1,7 +1,7 @@ -# Copyright (C) 2019 Project Quotient -# -# You may use this file under the terms of the LGPL-2.1 license -# See the file LICENSE from this package for details. +# SPDX-FileCopyrightText: 2019 Kitsune Ral +# SPDX-FileCopyrightText: 2019 Marc Deop + +# SPDX-License-Identifier: LGPL-2.1-or-later # This is the clang-format configuration style to be used by libQuotient. # Inspired by: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bda004df..f09b1529 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,8 +96,18 @@ All new contributed material that is not executable, including all text when not --> Any components proposed for reuse should have a license that permits releasing -a derivative work under *LGPL v2.1 or later* or LGPL v3. Moreover, the license of -a proposed component should be approved by OSI, no exceptions. +a derivative work under *LGPL v3 or later* (that includes licenses permitting +*LGPL v2.1 or later* but not *LGPL v2.1 only*). In any case, the component +should be redistributable under a license from +[the list approved by OSI](https://opensource.org/licenses), no exceptions. + +We use [SPDX](https://spdx.dev) conventions for copyright statements. Please +follow them when making a sizable contribution: add your name and year to +the top of the file. New files should begin with the following preamble: +```cpp +// SPDX-FileCopyrightText: 2021 Your Name +// SPDX-License-Identifier: LGPL-2.1-or-later +``` ## Vulnerability reporting (security issues) - see [SECURITY.md](./SECURITY.md) diff --git a/gtad/data.h.mustache b/gtad/data.h.mustache index 32ea85ee..a2193380 100644 --- a/gtad/data.h.mustache +++ b/gtad/data.h.mustache @@ -1,3 +1,7 @@ +{{! +SPDX-FileCopyrightText: 2020 Kitsune Ral +SPDX-License-Identifier: LGPL-2.1-or-later +}} {{>preamble}} #pragma once diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache index 3c3396e9..1d0ae476 100644 --- a/gtad/operation.cpp.mustache +++ b/gtad/operation.cpp.mustache @@ -1,3 +1,7 @@ +{{! +SPDX-FileCopyrightText: 2020 Kitsune Ral +SPDX-License-Identifier: LGPL-2.1-or-later +}} {{>preamble}} #include "{{filenameBase}}.h" diff --git a/gtad/operation.h.mustache b/gtad/operation.h.mustache index 36963b9a..135eee55 100644 --- a/gtad/operation.h.mustache +++ b/gtad/operation.h.mustache @@ -1,3 +1,7 @@ +{{! +SPDX-FileCopyrightText: 2020 Kitsune Ral +SPDX-License-Identifier: LGPL-2.1-or-later +}} {{>preamble}} #pragma once diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 0573df5d..77648562 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "avatar.h" diff --git a/lib/avatar.h b/lib/avatar.h index 111f565d..be125c17 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -46,4 +43,4 @@ private: }; } // namespace Quotient /// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; \ No newline at end of file +namespace QMatrixClient = Quotient; diff --git a/lib/connection.cpp b/lib/connection.cpp index 015e73c9..d773f0d8 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1,8 +1,8 @@ -/****************************************************************************** - * 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: 2019 Ville Ranki +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connection.h" diff --git a/lib/connection.h b/lib/connection.h index f3d7d725..4f949641 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -1,8 +1,7 @@ -/****************************************************************************** - * 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: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index 25ab775a..e54d909b 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connectiondata.h" diff --git a/lib/connectiondata.h b/lib/connectiondata.h index a3b2d39b..7dd96f26 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/converters.cpp b/lib/converters.cpp index 0df880a0..4338e8ed 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "converters.h" diff --git a/lib/converters.h b/lib/converters.h index d4f19b60..e07b6ee4 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/e2ee.h b/lib/e2ee.h index 5f1857b6..4044aa02 100644 --- a/lib/e2ee.h +++ b/lib/e2ee.h @@ -1,6 +1,5 @@ // SPDX-FileCopyrightText: 2019 Alexey Andreyev // SPDX-FileCopyrightText: 2019 Kitsune Ral -// // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 8d241eb2..37f3b7c3 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -1,6 +1,5 @@ // SPDX-FileCopyrightText: 2019 Alexey Andreyev // SPDX-FileCopyrightText: 2019 Kitsune Ral -// // SPDX-License-Identifier: LGPL-2.1-or-later #ifdef Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 0f507337..714f95fd 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2019 Alexey Andreyev -// // SPDX-License-Identifier: LGPL-2.1-or-later #ifdef Quotient_E2EE_ENABLED diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 9c47e50d..4f1595bc 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "eventitem.h" diff --git a/lib/eventitem.h b/lib/eventitem.h index 2d3d9ef6..1986ba77 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index d0abf577..8cea0ec8 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index f3d0a9a0..be83d9d0 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-License-Identifier: LGPL-2.1-or-later #include "callanswerevent.h" diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index d7214468..6132cb44 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp index 9b765064..b87c8e9b 100644 --- a/lib/events/callcandidatesevent.cpp +++ b/lib/events/callcandidatesevent.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-License-Identifier: LGPL-2.1-or-later #include "callcandidatesevent.h" diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index ae3bb150..b9de7556 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -1,8 +1,7 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp index 45b84cd4..43bc4db0 100644 --- a/lib/events/callhangupevent.cpp +++ b/lib/events/callhangupevent.cpp @@ -1,5 +1,6 @@ /****************************************************************************** * SPDX-FileCopyrightText: 2017 Marius Gripsgard + * SPDX-FileCopyrightText: 2018 Josip Delic * * SPDX-License-Identifier: LGPL-2.1-or-later */ diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index 432f72f5..24382ac2 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 86478ada..5ea54662 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-License-Identifier: LGPL-2.1-or-later #include "callinviteevent.h" diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 304c89ac..d3454c4f 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Marius Gripsgard +// SPDX-FileCopyrightText: 2018 Josip Delic +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index 39d11072..0ee1f7b0 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "directchatevent.h" diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index 373e36dc..e2143779 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index dc9eaf2d..0290f973 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2019 Alexey Andreyev -// // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 9de08b00..eb7123eb 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2019 Alexey Andreyev -// // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index f1bde621..490a5e8a 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "encryptionevent.h" #include "e2ee.h" diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 3431ddd8..f9bbab12 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 97edb4e0..3d66ab55 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "event.h" diff --git a/lib/events/event.h b/lib/events/event.h index c5752a7a..f8f8311d 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 18b1b94b..b249b160 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "eventcontent.h" diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index e247adbf..60d1f7b7 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 0d95daf5..978668f2 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp index 9b43e372..b53fffd6 100644 --- a/lib/events/reactionevent.cpp +++ b/lib/events/reactionevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "reactionevent.h" diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 09166b24..777905f2 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index b6f0fcdd..4185d92d 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later /* Example of a Receipt Event: diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index ec297a6c..4feec9ea 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp deleted file mode 100644 index 5889773c..00000000 --- a/lib/events/redactionevent.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Kitsune Ral -// -// SPDX-License-Identifier: CC0-1.0 - -#include "redactionevent.h" diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index 320db6f2..ed560331 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 649412e8..a4257895 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index eda94d2d..bb8654e5 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2020 QMatrixClient project - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2020 Ram Nad +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index 3d9ec4a3..6558bade 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 QMatrixClient project - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomcreateevent.h" diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 8328d38a..05e623ed 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 QMatrixClient project - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 2b6ac2be..4fec9d2b 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomevent.h" diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 3fafecfd..fea509c0 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 66580430..332be3f7 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "roomkeyevent.h" using namespace Quotient; diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index b8cd2eae..14e80324 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "event.h" diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index d093286c..9634ca3a 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-FileCopyrightText: 2019 Karol Kosek +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roommemberevent.h" diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index b7a7c9df..f2fbe689 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -1,8 +1,7 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-FileCopyrightText: 2019 Karol Kosek +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 19d460b8..14824277 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -1,8 +1,7 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-FileCopyrightText: 2017 Roman Plášil +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roommessageevent.h" diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index ebc9d564..8303ce4e 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -1,8 +1,7 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-FileCopyrightText: 2017 Roman Plášil +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp index 0a401752..8d262ddf 100644 --- a/lib/events/roompowerlevelsevent.cpp +++ b/lib/events/roompowerlevelsevent.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2019 Black Hat +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "roompowerlevelsevent.h" #include diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index b832230e..0346fc0d 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2019 Black Hat +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "eventcontent.h" diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp index 163e1d3a..080d269c 100644 --- a/lib/events/roomtombstoneevent.cpp +++ b/lib/events/roomtombstoneevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 QMatrixClient project - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomtombstoneevent.h" diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 8d50aba0..30e53738 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 QMatrixClient project - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 58ba3b5a..d6261a8f 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 7bde12bb..42fc9054 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "stateevent.h" diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 0db37767..1415f709 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp index 7d3f71e5..e97e978f 100644 --- a/lib/events/typingevent.cpp +++ b/lib/events/typingevent.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "typingevent.h" diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 8ca4f8e4..7456100a 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a0c88581..48c2996d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "basejob.h" diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 3165edd3..ca91a781 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 0011a97c..0b0531ad 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "downloadfilejob.h" #include diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index e00fd9e4..0752af89 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "csapi/content-repo.h" diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index fbea8797..7dbf4ab3 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "mediathumbnailjob.h" diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index cb55a0b0..3183feb1 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index cec15954..047e2920 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "requestdata.h" #include diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 2a227646..4958e0f9 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index beb0a535..59a34ef3 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "syncjob.h" diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index a7d10ed8..830a7c71 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/joinstate.h b/lib/joinstate.h index 1a7b1add..805ce73a 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/logging.cpp b/lib/logging.cpp index 3f757393..af229684 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Elvis Angelaccio - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Elvis Angelaccio +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "logging.h" diff --git a/lib/logging.h b/lib/logging.h index 21d05d8b..432ed16f 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Elvis Angelaccio +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 43a8287a..a94ead34 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "networkaccessmanager.h" diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 6075767a..47729a1b 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index db16034a..ce46ce5f 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "networksettings.h" diff --git a/lib/networksettings.h b/lib/networksettings.h index 31602734..df11a9c8 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 158d7a40..c6fa037a 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2019 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/quotient_common.h b/lib/quotient_common.h index e2384f12..22fdbe94 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2019 Kitsune Ral -// // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once 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" diff --git a/lib/room.h b/lib/room.h index c9205e9c..a8275ce9 100644 --- a/lib/room.h +++ b/lib/room.h @@ -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 #pragma once diff --git a/lib/settings.cpp b/lib/settings.cpp index dd086d9c..703f4320 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "settings.h" #include "logging.h" diff --git a/lib/settings.h b/lib/settings.h index badabec2..84c54802 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp index 3c6ec48b..a1d27504 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "ssosession.h" #include "connection.h" diff --git a/lib/ssosession.h b/lib/ssosession.h index 5845cd4d..72dd60c4 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index f67ab6c7..adcba5cd 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "syncdata.h" diff --git a/lib/syncdata.h b/lib/syncdata.h index d9868e46..e69bac17 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -1,8 +1,5 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/uri.cpp b/lib/uri.cpp index e0912eb6..4b171e79 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "uri.h" #include "logging.h" diff --git a/lib/uri.h b/lib/uri.h index 270766dd..d8b892b6 100644 --- a/lib/uri.h +++ b/lib/uri.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "quotient_common.h" diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp index 27360bcc..287e0552 100644 --- a/lib/uriresolver.cpp +++ b/lib/uriresolver.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "uriresolver.h" #include "connection.h" diff --git a/lib/uriresolver.h b/lib/uriresolver.h index 428ce04c..f290e58b 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "uri.h" diff --git a/lib/user.cpp b/lib/user.cpp index 9c2b76b5..bed7be3a 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "user.h" diff --git a/lib/user.h b/lib/user.h index d5c892ed..f831865e 100644 --- a/lib/user.h +++ b/lib/user.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2015 Felix Rohrbach - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/lib/util.cpp b/lib/util.cpp index 14492ba6..904bfd5a 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2018 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later #include "util.h" diff --git a/lib/util.h b/lib/util.h index 7547a75a..cb0ff44a 100644 --- a/lib/util.h +++ b/lib/util.h @@ -1,8 +1,6 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2016 Kitsune Ral - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 98c01cfc..5098bc02 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connection.h" #include "room.h" -- cgit v1.2.3 From 17e62b85cae99d8485be44f90f0622e4ba843fa0 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 10 Jan 2021 23:26:57 +0100 Subject: Add more properties to CallCandidateEvent --- lib/events/callcandidatesevent.h | 11 +++++++++ tests/callcandidateseventtest.cpp | 47 +++++++++++++++++++++++++++++++++++++++ tests/callcandidateseventtest.h | 13 +++++++++++ 3 files changed, 71 insertions(+) create mode 100644 tests/callcandidateseventtest.cpp create mode 100644 tests/callcandidateseventtest.h diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index b9de7556..c2ccac3b 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2017 Marius Gripsgard // SPDX-FileCopyrightText: 2018 Josip Delic // SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-FileCopyrightText: 2020 Carl Schwan // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -26,6 +27,16 @@ public: { return content("candidates"_ls); } + + QString callId() const + { + return content("call_id"); + } + + int version() const + { + return content("version"); + } }; REGISTER_EVENT_TYPE(CallCandidatesEvent) diff --git a/tests/callcandidateseventtest.cpp b/tests/callcandidateseventtest.cpp new file mode 100644 index 00000000..27108fdd --- /dev/null +++ b/tests/callcandidateseventtest.cpp @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "events/callcandidatesevent.h" +#include "callcandidateseventtest.h" + +void TestCallCandidatesEvent::fromJson() +{ + auto documemt = QJsonDocument::fromJson(R"({ + "age": 242352, + "content": { + "call_id": "12345", + "candidates": [ + { + "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0", + "sdpMLineIndex": 0, + "sdpMid": "audio" + } + ], + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.candidates" + })"); + + QVERIFY(documemt.isObject()); + + auto object = documemt.object(); + + Quotient::CallCandidatesEvent callCandidatesEvent(object); + + QCOMPARE(callCandidatesEvent.version(), 0); + QCOMPARE(callCandidatesEvent.callId(), "12345"); + QCOMPARE(callCandidatesEvent.candidates().count(), 1); + + const QJsonObject &candidate = callCandidatesEvent.candidates().at(0).toObject(); + QCOMPARE(candidate.value("sdpMid").toString(), "audio"); + QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0); + QCOMPARE(candidate.value("candidate").toString(), "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0"); +} + +QTEST_MAIN(TestCallCandidatesEvent) +#include "callcandidateseventtest.moc" diff --git a/tests/callcandidateseventtest.h b/tests/callcandidateseventtest.h new file mode 100644 index 00000000..b81c9c9b --- /dev/null +++ b/tests/callcandidateseventtest.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +class TestCallCandidatesEvent : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void fromJson(); +}; -- cgit v1.2.3 From 0368242c543d388b789eb02f22552128e3b14c18 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 11 Jan 2021 23:26:08 +0100 Subject: Apply suggestions from code review Co-authored-by: Kitsune Ral --- tests/callcandidateseventtest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/callcandidateseventtest.cpp b/tests/callcandidateseventtest.cpp index 27108fdd..ca908db3 100644 --- a/tests/callcandidateseventtest.cpp +++ b/tests/callcandidateseventtest.cpp @@ -7,7 +7,7 @@ void TestCallCandidatesEvent::fromJson() { - auto documemt = QJsonDocument::fromJson(R"({ + auto document = QJsonDocument::fromJson(R"({ "age": 242352, "content": { "call_id": "12345", @@ -27,7 +27,7 @@ void TestCallCandidatesEvent::fromJson() "type": "m.call.candidates" })"); - QVERIFY(documemt.isObject()); + QVERIFY(document.isObject()); auto object = documemt.object(); -- cgit v1.2.3 From a9987b1bc7f789f3063c06ed23e1f51024341463 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 21 Jan 2021 19:16:31 +0100 Subject: Move tests --- CMakeLists.txt | 10 +- autotests/CMakeLists.txt | 14 + autotests/callcandidateseventtest.cpp | 47 ++ autotests/callcandidateseventtest.h | 13 + quotest/.valgrind.supp | 68 +++ quotest/CMakeLists.txt | 9 + quotest/quotest.cpp | 864 ++++++++++++++++++++++++++++++++++ tests/.valgrind.supp | 68 --- tests/CMakeLists.txt | 71 --- tests/callcandidateseventtest.cpp | 47 -- tests/callcandidateseventtest.h | 13 - tests/quotest.cpp | 864 ---------------------------------- 12 files changed, 1020 insertions(+), 1068 deletions(-) create mode 100644 autotests/CMakeLists.txt create mode 100644 autotests/callcandidateseventtest.cpp create mode 100644 autotests/callcandidateseventtest.h create mode 100644 quotest/.valgrind.supp create mode 100644 quotest/CMakeLists.txt create mode 100644 quotest/quotest.cpp delete mode 100644 tests/.valgrind.supp delete mode 100644 tests/CMakeLists.txt delete mode 100644 tests/callcandidateseventtest.cpp delete mode 100644 tests/callcandidateseventtest.h delete mode 100644 tests/quotest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 29dea14a..0bc3a559 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -259,8 +259,6 @@ endif() # new files and if it has, re-run cmake. file(GLOB_RECURSE api_SRCS ${add_CONFIGURE_DEPENDS} ${FULL_CSAPI_DIR}/*.cpp) -set(tests_SRCS tests/quotest.cpp) - add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" @@ -290,9 +288,11 @@ if (${PROJECT_NAME}_ENABLE_E2EE) endif() target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) -set(TEST_BINARY quotest) -add_executable(${TEST_BINARY} ${tests_SRCS}) -target_link_libraries(${TEST_BINARY} Qt5::Core Qt5::Test ${PROJECT_NAME}) +if (BUILD_TESTING) + enable_testing() + add_subdirectory(quotest) + add_subdirectory(autotests) +endif() configure_file(${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc @ONLY NEWLINE_STYLE UNIX) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 00000000..abfcb49a --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2021 Carl Schwan +# +# SPDX-License-Identifier: BSD-3-Clause + +include(CMakeParseArguments) + +function(QUOTIENT_ADD_TEST) + cmake_parse_arguments(ARG "" "NAME" "" ${ARGN}) + add_executable(${ARG_NAME} ${ARG_NAME}.cpp) + target_link_libraries(${ARG_NAME} Qt5::Core Qt5::Test Quotient) + add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME}) +endfunction() + +quotient_add_test(callcandidateseventtest.cpp NAME callcandidateseventtest) diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp new file mode 100644 index 00000000..ca908db3 --- /dev/null +++ b/autotests/callcandidateseventtest.cpp @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "events/callcandidatesevent.h" +#include "callcandidateseventtest.h" + +void TestCallCandidatesEvent::fromJson() +{ + auto document = QJsonDocument::fromJson(R"({ + "age": 242352, + "content": { + "call_id": "12345", + "candidates": [ + { + "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0", + "sdpMLineIndex": 0, + "sdpMid": "audio" + } + ], + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.candidates" + })"); + + QVERIFY(document.isObject()); + + auto object = documemt.object(); + + Quotient::CallCandidatesEvent callCandidatesEvent(object); + + QCOMPARE(callCandidatesEvent.version(), 0); + QCOMPARE(callCandidatesEvent.callId(), "12345"); + QCOMPARE(callCandidatesEvent.candidates().count(), 1); + + const QJsonObject &candidate = callCandidatesEvent.candidates().at(0).toObject(); + QCOMPARE(candidate.value("sdpMid").toString(), "audio"); + QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0); + QCOMPARE(candidate.value("candidate").toString(), "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0"); +} + +QTEST_MAIN(TestCallCandidatesEvent) +#include "callcandidateseventtest.moc" diff --git a/autotests/callcandidateseventtest.h b/autotests/callcandidateseventtest.h new file mode 100644 index 00000000..b81c9c9b --- /dev/null +++ b/autotests/callcandidateseventtest.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +class TestCallCandidatesEvent : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void fromJson(); +}; diff --git a/quotest/.valgrind.supp b/quotest/.valgrind.supp new file mode 100644 index 00000000..d65fb52e --- /dev/null +++ b/quotest/.valgrind.supp @@ -0,0 +1,68 @@ +{ + libc_dirty_free_on_exit + Memcheck:Free + fun:free + fun:__libc_freeres + fun:_vgnU_freeres + fun:__run_exit_handlers + fun:exit +} + +{ + QAuthenticator + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_ZN14QAuthenticator6detachEv +} + +{ + QTimer + Memcheck:Leak + match-leak-kinds: possible + fun:_Znwm + fun:_ZN7QObjectC1EPS_ + fun:_ZN6QTimerC1EP7QObject +} + +{ + QSslConfiguration + Memcheck:Leak + match-leak-kinds: possible + fun:_Znwm + ... + fun:_ZN17QSslConfigurationC1Ev +} + +{ + libcrypto_ASN1 + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:ASN1_item_ex_d2i +} + +{ + malloc_from_libcrypto + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:CRYPTO_malloc + ... + obj:/lib/x86_64-linux-gnu/libcrypto.so.* +} + +{ + Slot_activation_from_QtNetwork + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:inflateInit2_ + obj:/*/*/*/libQt5Network.so.* + ... + fun:_ZN11QMetaObject8activateEP7QObjectiiPPv + ... + fun:_ZN11QMetaObject8activateEP7QObjectiiPPv + obj:/*/*/*/libQt5Network.so.* +} \ No newline at end of file diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt new file mode 100644 index 00000000..4553abb6 --- /dev/null +++ b/quotest/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Carl Schwan +# +# SPDX-License-Identifier: BSD-3-Clause + +set(quotest_SRCS quotest.cpp) + +add_executable(quotest ${quotest_SRCS}) +add_test(NAME quotest COMMAND quotest) +target_link_libraries(quotest PRIVATE Qt5::Core Qt5::Test ${PROJECT_NAME}) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp new file mode 100644 index 00000000..5098bc02 --- /dev/null +++ b/quotest/quotest.cpp @@ -0,0 +1,864 @@ +// SPDX-FileCopyrightText: 2016 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "connection.h" +#include "room.h" +#include "user.h" +#include "uriresolver.h" + +#include "csapi/joining.h" +#include "csapi/leaving.h" +#include "csapi/room_send.h" + +#include "events/reactionevent.h" +#include "events/redactionevent.h" +#include "events/simplestateevents.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace Quotient; +using std::clog, std::endl; + +class TestSuite; + +class TestManager : public QCoreApplication { +public: + TestManager(int& argc, char** argv); + +private: + void setupAndRun(); + void onNewRoom(Room* r); + void doTests(); + void conclude(); + void finalize(); + +private: + Connection* c = nullptr; + QString origin; + QString targetRoomName; + TestSuite* testSuite = nullptr; + QByteArrayList running {}, succeeded {}, failed {}; +}; + +using TestToken = QByteArray; // return value of QMetaMethod::name +Q_DECLARE_METATYPE(TestToken) + +// For now, the token itself is the test name but that may change. +const char* testName(const TestToken& token) { return token.constData(); } + +/// Test function declaration +/*! + * \return true, if the test finished (successfully or unsuccessfully); + * false, if the test went async and will complete later + */ +#define TEST_DECL(Name) bool Name(const TestToken& thisTest); + +/// The holder for the actual tests +/*! + * This class takes inspiration from Qt Test in terms of tests invocation; + * TestManager instantiates it and runs all public slots (cf. private slots in + * Qt Test) one after another. An important diversion from Qt Test is that + * the tests are assumed to by asynchronous rather than synchronous; so it's + * perfectly normal to have a few tests running at the same time. To avoid + * context clashes a special parameter with the name thisTest is passed to + * each test. Each test must conclude (synchronously or asynchronously) with + * an invocation of FINISH_TEST() macro (or FAIL_TEST() macro that expands to + * FINISH_TEST) that expects thisTest variable to be reachable. If FINISH_TEST() + * is invoked twice with the same thisTest, the second call will cause assertion + * failure; if FINISH_TEST() is not invoked at all, the test will be killed + * by a watchdog after a timeout and marked in the final report as not finished. + */ +class TestSuite : public QObject { + Q_OBJECT +public: + TestSuite(Room* testRoom, QString source, TestManager* parent) + : QObject(parent), targetRoom(testRoom), origin(std::move(source)) + { + qRegisterMetaType(); + Q_ASSERT(testRoom && parent); + } + +signals: + void finishedItem(QByteArray /*name*/, bool /*condition*/); + +public slots: + void doTest(const QByteArray& testName); + +private slots: + TEST_DECL(findRoomByAlias) + TEST_DECL(loadMembers) + TEST_DECL(sendMessage) + TEST_DECL(sendReaction) + TEST_DECL(sendFile) + TEST_DECL(setTopic) + TEST_DECL(changeName) + TEST_DECL(sendAndRedact) + TEST_DECL(addAndRemoveTag) + TEST_DECL(markDirectChat) + TEST_DECL(visitResources) + // Add more tests above here + +public: + [[nodiscard]] Room* room() const { return targetRoom; } + [[nodiscard]] Connection* connection() const + { + return targetRoom->connection(); + } + +private: + [[nodiscard]] bool checkFileSendingOutcome(const TestToken& thisTest, + const QString& txnId, + const QString& fileName); + [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact); + + [[nodiscard]] bool validatePendingEvent(const QString& txnId); + [[nodiscard]] bool checkDirectChat() const; + void finishTest(const TestToken& token, bool condition, const char* file, + int line); + +private: + Room* targetRoom; + QString origin; +}; + +#define TEST_IMPL(Name) bool TestSuite::Name(const TestToken& thisTest) + +// Returning true (rather than a void) allows to reuse the convention with +// connectUntil() to break the QMetaObject::Connection upon finishing the test +// item. +#define FINISH_TEST(Condition) \ + return (finishTest(thisTest, Condition, __FILE__, __LINE__), true) + +#define FAIL_TEST() FINISH_TEST(false) + +void TestSuite::doTest(const QByteArray& testName) +{ + clog << "Starting: " << testName.constData() << endl; + QMetaObject::invokeMethod(this, testName, Qt::DirectConnection, + Q_ARG(TestToken, testName)); +} + +bool TestSuite::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() + && it->deliveryStatus() == EventStatus::Submitted + && (*it)->transactionId() == txnId; +} + +void TestSuite::finishTest(const TestToken& token, bool condition, + const char* file, int line) +{ + const auto& item = testName(token); + if (condition) { + clog << item << " successful" << endl; + if (targetRoom) + targetRoom->postMessage(origin % ": " % item % " successful", + MessageEventType::Notice); + } else { + clog << item << " FAILED at " << file << ":" << line << endl; + if (targetRoom) + targetRoom->postPlainText(origin % ": " % item % " FAILED at " + % file % ", line " % QString::number(line)); + } + + emit finishedItem(item, condition); +} + +TestManager::TestManager(int& argc, char** argv) + : QCoreApplication(argc, argv), c(new Connection(this)) +{ + Q_ASSERT(argc >= 5); + clog << "Connecting to Matrix as " << argv[1] << endl; + c->loginWithPassword(argv[1], argv[2], argv[3]); + targetRoomName = argv[4]; + clog << "Test room name: " << argv[4] << endl; + if (argc > 5) { + origin = argv[5]; + clog << "Origin for the test message: " << origin.toStdString() << endl; + } + + connect(c, &Connection::connected, this, &TestManager::setupAndRun); + connect(c, &Connection::resolveError, this, + [](const QString& error) { + clog << "Failed to resolve the server: " << error.toStdString() + << endl; + QCoreApplication::exit(-2); + }, + Qt::QueuedConnection); + connect(c, &Connection::loginError, this, + [this](const QString& message, const QString& details) { + clog << "Failed to login to " + << c->homeserver().toDisplayString().toStdString() << ": " + << message.toStdString() << endl + << "Details:" << endl + << details.toStdString() << endl; + QCoreApplication::exit(-2); + }, + Qt::QueuedConnection); + connect(c, &Connection::loadedRoomState, this, &TestManager::onNewRoom); + + // Big countdown watchdog + QTimer::singleShot(180000, this, [this] { + if (testSuite) + conclude(); + else + finalize(); + }); +} + +void TestManager::setupAndRun() +{ + Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); + Q_ASSERT(c->domain() == c->userId().section(':', 1)); + clog << "Connected, server: " + << c->homeserver().toDisplayString().toStdString() << endl; + clog << "Access token: " << c->accessToken().toStdString() << endl; + + c->setLazyLoading(true); + + clog << "Joining " << targetRoomName.toStdString() << endl; + auto joinJob = c->joinRoom(targetRoomName); + // Ensure that the room has been joined and filled with some events + // so that other tests could use that + connect(joinJob, &BaseJob::success, this, [this, joinJob] { + testSuite = new TestSuite(c->room(joinJob->roomId()), origin, this); + // Only start the sync after joining, to make sure the room just + // joined is in it + c->syncLoop(); + connect(c, &Connection::syncDone, this, [this] { + static int i = 0; + clog << "Sync " << ++i << " complete" << endl; + if (auto* r = testSuite->room()) { + clog << "Test room timeline size = " << r->timelineSize(); + if (r->pendingEvents().empty()) + clog << ", pending size = " << r->pendingEvents().size(); + clog << endl; + } + if (!running.empty()) { + clog << running.size() << " test(s) in the air:"; + for (const auto& test: qAsConst(running)) + clog << " " << testName(test); + clog << endl; + } + if (i == 1) { + testSuite->room()->getPreviousContent(); + connectSingleShot(testSuite->room(), &Room::addedMessages, this, + &TestManager::doTests); + } + }); + }); + connect(joinJob, &BaseJob::failure, this, [this] { + clog << "Failed to join the test room" << endl; + finalize(); + }); +} + +void TestManager::onNewRoom(Room* r) +{ + clog << "New room: " << r->id().toStdString() << endl + << " Name: " << r->name().toStdString() << endl + << " Canonical alias: " << r->canonicalAlias().toStdString() << endl + << endl; + connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { + clog << timeline.size() << " new event(s) in room " + << r->objectName().toStdString() << endl; + }); +} + +void TestManager::doTests() +{ + const auto* metaObj = testSuite->metaObject(); + for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); ++i) { + const auto metaMethod = metaObj->method(i); + if (metaMethod.access() != QMetaMethod::Private + || metaMethod.methodType() != QMetaMethod::Slot) + continue; + + const auto testName = metaMethod.name(); + running.push_back(testName); + // Some tests return the result immediately but we queue everything + // and process all tests asynchronously. + QMetaObject::invokeMethod(testSuite, "doTest", Qt::QueuedConnection, + Q_ARG(QByteArray, testName)); + } + clog << "Tests to do:"; + for (const auto& test: qAsConst(running)) + clog << " " << testName(test); + clog << endl; + connect(testSuite, &TestSuite::finishedItem, this, + [this](const QByteArray& itemName, bool condition) { + if (auto i = running.indexOf(itemName); i != -1) + (condition ? succeeded : failed).push_back(running.takeAt(i)); + else + Q_ASSERT_X(false, itemName, + "Test item is not in running state"); + if (running.empty()) { + clog << "All tests finished" << endl; + conclude(); + } + }); +} + +TEST_IMPL(findRoomByAlias) +{ + auto* roomByAlias = connection()->roomByAlias(targetRoom->canonicalAlias(), + JoinState::Join); + FINISH_TEST(roomByAlias == targetRoom); +} + +TEST_IMPL(loadMembers) +{ + // It's not exactly correct because an arbitrary server might not support + // lazy loading; but in the absence of capabilities framework we assume + // it does. + if (targetRoom->users().size() >= targetRoom->joinedCount()) { + clog << "Lazy loading doesn't seem to be enabled" << endl; + FAIL_TEST(); + } + targetRoom->setDisplayed(); + connect(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { + FINISH_TEST(targetRoom->users().size() >= targetRoom->joinedCount()); + }); + return false; +} + +TEST_IMPL(sendMessage) +{ + auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); + if (!validatePendingEvent(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId](const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + FINISH_TEST(is(*evt) && !evt->id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + }); + return false; +} + +TEST_IMPL(sendReaction) +{ + clog << "Reacting to the newest message in the room" << endl; + Q_ASSERT(targetRoom->timelineSize() > 0); + const auto targetEvtId = targetRoom->messageEvents().back()->id(); + const auto key = QStringLiteral("+1"); + const auto txnId = targetRoom->postReaction(targetEvtId, key); + if (!validatePendingEvent(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + + connectUntil(targetRoom, &Room::updatedEvent, this, + [this, thisTest, txnId, key, targetEvtId](const QString& actualTargetEvtId) { + if (actualTargetEvtId != targetEvtId) + return false; + const auto reactions = targetRoom->relatedEvents( + targetEvtId, EventRelation::Annotation()); + // It's a test room, assuming no interference there should + // be exactly one reaction + if (reactions.size() != 1) + FAIL_TEST(); + + const auto* evt = + eventCast(reactions.back()); + FINISH_TEST(is(*evt) && !evt->id().isEmpty() + && evt->relation().key == key + && evt->transactionId() == txnId); + // TODO: Test removing the reaction + }); + return false; +} + +TEST_IMPL(sendFile) +{ + auto* tf = new QTemporaryFile; + if (!tf->open()) { + clog << "Failed to create a temporary file" << endl; + FAIL_TEST(); + } + tf->write("Test"); + tf->close(); + // QFileInfo::fileName brings only the file name; QFile::fileName brings + // the full path + const auto tfName = QFileInfo(*tf).fileName(); + clog << "Sending file " << tfName.toStdString() << endl; + const auto txnId = + targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName())); + if (!validatePendingEvent(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + delete tf; + FAIL_TEST(); + } + + // Using tf as a context object to clean away both connections + // once either of them triggers. + connectUntil(targetRoom, &Room::fileTransferCompleted, tf, + [this, thisTest, txnId, tf, tfName](const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); + + if (id != txnId) + return false; + + tf->deleteLater(); + return checkFileSendingOutcome(thisTest, txnId, tfName); + }); + connectUntil(targetRoom, &Room::fileTransferFailed, tf, + [this, thisTest, txnId, tf](const QString& id, const QString& error) { + if (id != txnId) + return false; + + targetRoom->postPlainText(origin % ": File upload failed: " % error); + tf->deleteLater(); + FAIL_TEST(); + }); + return false; +} + +bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, + const QString& txnId, + const QString& fileName) +{ + auto it = targetRoom->findPendingEvent(txnId); + if (it == targetRoom->pendingEvents().end()) { + clog << "Pending file event dropped before upload completion" << endl; + FAIL_TEST(); + } + if (it->deliveryStatus() != EventStatus::FileUploaded) { + clog << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + FAIL_TEST(); + } + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId, fileName](const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + clog << "File event " << txnId.toStdString() + << " arrived in the timeline" << endl; + // This part tests visit() + return visit( + *evt, + [&](const RoomMessageEvent& e) { + // TODO: actually try to download it to check, e.g., #366 + // (and #368 would help to test against bad file names). + FINISH_TEST( + !e.id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == txnId + && e.hasFileContent() + && e.content()->fileInfo()->originalName == fileName); + }, + [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); + }); + return true; +} + +TEST_IMPL(setTopic) +{ + const auto newTopic = connection()->generateTxnId(); // Just a way to make + // a unique id + targetRoom->setTopic(newTopic); + connectUntil(targetRoom, &Room::topicChanged, this, + [this, thisTest, newTopic] { + if (targetRoom->topic() == newTopic) + FINISH_TEST(true); + + clog << "Requested topic was " << newTopic.toStdString() << ", " + << targetRoom->topic().toStdString() << " arrived instead" + << endl; + return false; + }); + return false; +} + +TEST_IMPL(changeName) +{ + auto* const localUser = connection()->user(); + const auto& newName = connection()->generateTxnId(); // See setTopic() + clog << "Renaming the user to " << newName.toStdString() << endl; + localUser->rename(newName); + connectUntil(localUser, &User::defaultNameChanged, this, + [this, thisTest, localUser, newName] { + FINISH_TEST(localUser->name() == newName); + }); + return false; +} + +TEST_IMPL(sendAndRedact) +{ + clog << "Sending a message to redact" << endl; + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) + FAIL_TEST(); + + connectUntil(targetRoom, &Room::messageSent, this, + [this, thisTest, txnId](const QString& tId, const QString& evtId) { + if (tId != txnId) + return false; + + // The event may end up having been merged, and that's ok; + // but if it's not, it has to be in the ReachedServer state. + if (auto it = room()->findPendingEvent(tId); + it != room()->pendingEvents().cend() + && it->deliveryStatus() != EventStatus::ReachedServer) { + clog << "Incorrect sent event status (" + << it->deliveryStatus() << ')' << endl; + FAIL_TEST(); + } + + clog << "Redacting the message" << endl; + targetRoom->redactEvent(evtId, origin); + connectUntil(targetRoom, &Room::addedMessages, this, + [this, thisTest, evtId] { + return checkRedactionOutcome(thisTest, evtId); + }); + return false; + }); + return false; +} + +bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact) +{ + // There are two possible (correct) outcomes: either the event comes already + // redacted at the next sync, or the nearest sync completes with + // the unredacted event but the next one brings redaction. + auto it = targetRoom->findInTimeline(evtIdToRedact); + if (it == targetRoom->timelineEdge()) + return false; // Waiting for the next sync + + if ((*it)->isRedacted()) { + clog << "The sync brought already redacted message" << endl; + FINISH_TEST(true); + } + + clog << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connectUntil(targetRoom, &Room::replacedEvent, this, + [this, thisTest, evtIdToRedact](const RoomEvent* newEvent, + const RoomEvent* oldEvent) { + if (oldEvent->id() != evtIdToRedact) + return false; + + FINISH_TEST(newEvent->isRedacted() + && newEvent->redactionReason() == origin); + }); + return true; +} + +TEST_IMPL(addAndRemoveTag) +{ + static const auto TestTag = QStringLiteral("im.quotient.test"); + // Pre-requisite + if (targetRoom->tags().contains(TestTag)) + targetRoom->removeTag(TestTag); + + // Unlike for most of Quotient, tags are applied and tagsChanged is emitted + // synchronously, with the server being notified async. The test checks + // that the signal is emitted, not only that tags have changed; but there's + // (currently) no way to check that the server has been correctly notified + // of the tag change. + QSignalSpy spy(targetRoom, &Room::tagsChanged); + targetRoom->addTag(TestTag); + if (spy.count() != 1 || !targetRoom->tags().contains(TestTag)) { + clog << "Tag adding failed" << endl; + FAIL_TEST(); + } + clog << "Test tag set, removing it now" << endl; + targetRoom->removeTag(TestTag); + FINISH_TEST(spy.count() == 2 && !targetRoom->tags().contains(TestTag)); +} + +bool TestSuite::checkDirectChat() const +{ + return targetRoom->directChatUsers().contains(connection()->user()); +} + +TEST_IMPL(markDirectChat) +{ + if (checkDirectChat()) + connection()->removeFromDirectChats(targetRoom->id(), + connection()->user()); + + int id = qRegisterMetaType(); // For QSignalSpy + Q_ASSERT(id != -1); + + // Same as with tags (and unusual for the rest of Quotient), direct chat + // operations are synchronous. + QSignalSpy spy(connection(), &Connection::directChatsListChanged); + clog << "Marking the room as a direct chat" << endl; + connection()->addToDirectChats(targetRoom, connection()->user()); + if (spy.count() != 1 || !checkDirectChat()) + FAIL_TEST(); + + // Check that the first argument (added DCs) actually contains the room + const auto& addedDCs = spy.back().front().value(); + if (addedDCs.size() != 1 + || !addedDCs.contains(connection()->user(), targetRoom->id())) { + clog << "The room is not in added direct chats" << endl; + FAIL_TEST(); + } + + clog << "Unmarking the direct chat" << endl; + connection()->removeFromDirectChats(targetRoom->id(), connection()->user()); + if (spy.count() != 2 && checkDirectChat()) + FAIL_TEST(); + + // Check that the second argument (removed DCs) actually contains the room + const auto& removedDCs = spy.back().back().value(); + FINISH_TEST(removedDCs.size() == 1 + && removedDCs.contains(connection()->user(), targetRoom->id())); +} + +TEST_IMPL(visitResources) +{ + // Same as the two tests above, ResourceResolver emits signals + // synchronously so we use signal spies to intercept them instead of + // connecting lambdas before calling openResource(). NB: this test + // assumes that ResourceResolver::openResource is implemented in terms + // of ResourceResolver::visitResource, so the latter doesn't need a + // separate test. + static UriDispatcher ud; + + // This lambda returns true in case of error, false if it's fine so far + auto testResourceResolver = [this, thisTest](const QStringList& uris, + auto signal, auto* target, + QVariantList otherArgs = {}) { + int r = qRegisterMetaType(); + Q_ASSERT(r != 0); + QSignalSpy spy(&ud, signal); + for (const auto& uriString: uris) { + Uri uri { uriString }; + clog << "Checking " << uriString.toStdString() + << " -> " << uri.toDisplayString().toStdString() << endl; + if (auto matrixToUrl = uri.toUrl(Uri::MatrixToUri).toDisplayString(); + !matrixToUrl.startsWith("https://matrix.to/#/")) { + clog << "Incorrect matrix.to representation:" + << matrixToUrl.toStdString() << endl; + } + ud.visitResource(connection(), uriString); + if (spy.count() != 1) { + clog << "Wrong number of signal emissions (" << spy.count() + << ')' << endl; + FAIL_TEST(); + } + const auto& emission = spy.front(); + Q_ASSERT(emission.count() >= 2); + if (emission.front().value() != target) { + clog << "Signal emitted with an incorrect target" << endl; + FAIL_TEST(); + } + if (!otherArgs.empty()) { + if (emission.size() < otherArgs.size() + 1) { + clog << "Emission doesn't include all arguments" << endl; + FAIL_TEST(); + } + for (auto i = 0; i < otherArgs.size(); ++i) + if (otherArgs[i] != emission[i + 1]) { + clog << "Mismatch in argument #" << i + 1 << endl; + FAIL_TEST(); + } + } + spy.clear(); + } + return false; + }; + + // Basic tests + for (const auto& u: { Uri {}, Uri { QUrl {} } }) + if (u.isValid() || !u.isEmpty()) { + clog << "Empty Matrix URI test failed" << endl; + FAIL_TEST(); + } + if (Uri { QStringLiteral("#") }.isValid()) { + clog << "Bare sigil URI test failed" << endl; + FAIL_TEST(); + } + QUrl invalidUrl { "https://" }; + invalidUrl.setAuthority("---:@@@"); + const Uri matrixUriFromInvalidUrl { invalidUrl }, + invalidMatrixUri { QStringLiteral("matrix:&invalid@") }; + if (matrixUriFromInvalidUrl.isEmpty() || matrixUriFromInvalidUrl.isValid()) { + clog << "Invalid Matrix URI test failed" << endl; + FAIL_TEST(); + } + if (invalidMatrixUri.isEmpty() || invalidMatrixUri.isValid()) { + clog << "Invalid sigil in a Matrix URI - test failed" << endl; + FAIL_TEST(); + } + + // Matrix identifiers used throughout all URI tests + const auto& roomId = room()->id(); + const auto& roomAlias = room()->canonicalAlias(); + const auto& userId = connection()->userId(); + const auto& eventId = room()->messageEvents().back()->id(); + Q_ASSERT(!roomId.isEmpty()); + Q_ASSERT(!roomAlias.isEmpty()); + Q_ASSERT(!userId.isEmpty()); + Q_ASSERT(!eventId.isEmpty()); + + const QStringList roomUris { + roomId, "matrix:roomid/" + roomId.mid(1), + "https://matrix.to/#/%21"/*`!`*/ + roomId.mid(1), + roomAlias, "matrix:room/" + roomAlias.mid(1), + "https://matrix.to/#/" + roomAlias, + }; + const QStringList userUris { userId, "matrix:user/" + userId.mid(1), + "https://matrix.to/#/" + userId }; + const QStringList eventUris { + "matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1), + "https://matrix.to/#/" + roomId + '/' + eventId + }; + // Check that reserved characters are correctly processed. + static const auto& joinRoomAlias = + QStringLiteral("##/?.@\"unjoined:example.org"); + static const auto& encodedRoomAliasNoSigil = + QUrl::toPercentEncoding(joinRoomAlias.mid(1), ":"); + static const QString joinQuery { "?action=join" }; + // These URIs are not supposed to be actually joined (and even exist, + // as yet) - only to be syntactically correct + static const QStringList joinByAliasUris { + Uri(joinRoomAlias.toUtf8(), {}, joinQuery.mid(1)).toDisplayString(), + "matrix:room/" + encodedRoomAliasNoSigil + joinQuery, + "https://matrix.to/#/%23"/*`#`*/ + encodedRoomAliasNoSigil + joinQuery, + "https://matrix.to/#/%23" + joinRoomAlias.mid(1) /* unencoded */ + joinQuery + }; + static const auto& joinRoomId = QStringLiteral("!anyid:example.org"); + static const QStringList viaServers { "matrix.org", "example.org" }; + static const auto viaQuery = + std::accumulate(viaServers.cbegin(), viaServers.cend(), joinQuery, + [](const QString& q, const QString& s) { + return q + "&via=" + s; + }); + static const QStringList joinByIdUris { + "matrix:roomid/" + joinRoomId.mid(1) + viaQuery, + "https://matrix.to/#/" + joinRoomId + viaQuery + }; + // If any test breaks, the breaking call will return true, and further + // execution will be cut by ||'s short-circuiting + if (testResourceResolver(roomUris, &UriDispatcher::roomAction, room()) + || testResourceResolver(userUris, &UriDispatcher::userAction, + connection()->user()) + || testResourceResolver(eventUris, &UriDispatcher::roomAction, + room(), { eventId }) + || testResourceResolver(joinByAliasUris, &UriDispatcher::joinAction, + connection(), { joinRoomAlias }) + || testResourceResolver(joinByIdUris, &UriDispatcher::joinAction, + connection(), { joinRoomId, viaServers })) + return true; + // TODO: negative cases + FINISH_TEST(true); +} + +void TestManager::conclude() +{ + // Clean up the room (best effort) + auto* room = testSuite->room(); + room->setTopic({}); + room->localUser()->rename({}); + + QString succeededRec { QString::number(succeeded.size()) % " of " + % QString::number(succeeded.size() + failed.size() + + running.size()) + % " tests succeeded" }; + QString plainReport = origin % ": Testing complete, " % succeededRec; + QString color = failed.empty() && running.empty() ? "00AA00" : "AA0000"; + QString htmlReport = origin % ": Testing complete, " % succeededRec; + if (!failed.empty()) { + QByteArray failedList; + for (const auto& f : qAsConst(failed)) + failedList += ' ' + f; + plainReport += "\nFAILED:" + failedList; + htmlReport += "
Failed:" + failedList; + } + if (!running.empty()) { + QByteArray dnfList; + for (const auto& r : qAsConst(running)) + dnfList += ' ' + r; + plainReport += "\nDID NOT FINISH:" + dnfList; + htmlReport += "
Did not finish:" + dnfList; + } + + auto txnId = room->postHtmlText(plainReport, htmlReport); + // Now just wait until all the pending events reach the server + connectUntil(room, &Room::messageSent, this, + [this, txnId, room, plainReport] (const QString& sentTxnId) { + if (sentTxnId != txnId) + return false; + const auto& pendingEvents = room->pendingEvents(); + if (auto c = std::count_if(pendingEvents.cbegin(), + pendingEvents.cend(), + [](const PendingEventItem& pe) { + return pe.deliveryStatus() + < EventStatus::ReachedServer; + }); + c > 0) { + clog << "Events to reach the server: " << c + << ", not leaving yet" << endl; + return false; + } + + clog << "Leaving the room" << endl; + // TODO: Waiting for proper futures to come so that it could be: +// room->leaveRoom() +// .then(this, &TestManager::finalize); // Qt-style or +// .then([this] { finalize(); }); // STL-style + auto* job = room->leaveRoom(); + connect(job, &BaseJob::result, this, [this, job,plainReport] { + Q_ASSERT(job->status().good()); + finalize(); + // Still flying, as the exit() connection in finalize() is queued + clog << plainReport.toStdString() << endl; + }); + return true; + }); +} + +void TestManager::finalize() +{ + clog << "Logging out" << endl; + c->logout(); + connect(c, &Connection::loggedOut, this, + [this] { QCoreApplication::exit(failed.size() + running.size()); }, + Qt::QueuedConnection); +} + +int main(int argc, char* argv[]) +{ + // TODO: use QCommandLineParser + if (argc < 5) { + clog << "Usage: quotest [origin]" + << endl; + return -1; + } + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + return TestManager(argc, argv).exec(); +} + +#include "quotest.moc" diff --git a/tests/.valgrind.supp b/tests/.valgrind.supp deleted file mode 100644 index d65fb52e..00000000 --- a/tests/.valgrind.supp +++ /dev/null @@ -1,68 +0,0 @@ -{ - libc_dirty_free_on_exit - Memcheck:Free - fun:free - fun:__libc_freeres - fun:_vgnU_freeres - fun:__run_exit_handlers - fun:exit -} - -{ - QAuthenticator - Memcheck:Leak - match-leak-kinds: possible - ... - fun:_ZN14QAuthenticator6detachEv -} - -{ - QTimer - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - fun:_ZN7QObjectC1EPS_ - fun:_ZN6QTimerC1EP7QObject -} - -{ - QSslConfiguration - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - ... - fun:_ZN17QSslConfigurationC1Ev -} - -{ - libcrypto_ASN1 - Memcheck:Leak - match-leak-kinds: definite - fun:malloc - ... - fun:ASN1_item_ex_d2i -} - -{ - malloc_from_libcrypto - Memcheck:Leak - match-leak-kinds: possible - fun:malloc - fun:CRYPTO_malloc - ... - obj:/lib/x86_64-linux-gnu/libcrypto.so.* -} - -{ - Slot_activation_from_QtNetwork - Memcheck:Leak - match-leak-kinds: definite - fun:malloc - fun:inflateInit2_ - obj:/*/*/*/libQt5Network.so.* - ... - fun:_ZN11QMetaObject8activateEP7QObjectiiPPv - ... - fun:_ZN11QMetaObject8activateEP7QObjectiiPPv - obj:/*/*/*/libQt5Network.so.* -} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt deleted file mode 100644 index cb8c99f8..00000000 --- a/tests/CMakeLists.txt +++ /dev/null @@ -1,71 +0,0 @@ -cmake_minimum_required(VERSION 3.1) - -# This CMakeLists file assumes that the library is installed to CMAKE_INSTALL_PREFIX -# and ignores the in-tree library code. You can use this to start work on your own client. - -project(quotest CXX) - -include(CheckCXXCompilerFlag) -if (NOT WIN32) - include(GNUInstallDirs) -endif(NOT WIN32) - -# Find includes in corresponding build directories -set(CMAKE_INCLUDE_CURRENT_DIR ON) -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) - -# Set a default build type if none was specified -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to 'Debug' as none was specified") - set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) - # Set the possible values of build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" - "MinSizeRel" "RelWithDebInfo") -endif() - -if (NOT CMAKE_INSTALL_LIBDIR) - set(CMAKE_INSTALL_LIBDIR ".") -endif() - -if (NOT CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR ".") -endif() - -if (NOT CMAKE_INSTALL_INCLUDEDIR) - set(CMAKE_INSTALL_INCLUDEDIR "include") -endif() - -set(CMAKE_CXX_STANDARD 14) - -foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) - CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) - if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") - endif () -endforeach () - -find_package(Qt5 5.9 REQUIRED Network Gui Multimedia Test) -get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) - -set(LIBRARY_NAME "Quotient") - -find_package(${LIBRARY_NAME} REQUIRED) -get_filename_component(Quotient_Prefix "${Quotient_DIR}/../.." ABSOLUTE) - -message( STATUS "${PROJECT_NAME} configuration:" ) -if (CMAKE_BUILD_TYPE) - message( STATUS " Build type: ${CMAKE_BUILD_TYPE}") -endif(CMAKE_BUILD_TYPE) -message( STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) -message( STATUS " Qt: ${Qt5_VERSION} at ${Qt5_Prefix}" ) -message( STATUS " ${LIBRARY_NAME}: ${${LIBRARY_NAME}_VERSION} at ${${LIBRARY_NAME}_Prefix}" ) - -set(example_SRCS quotest.cpp) - -add_executable(${PROJECT_NAME} ${example_SRCS}) -target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Test ${LIBRARY_NAME}) - -# Installation - -install (TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/tests/callcandidateseventtest.cpp b/tests/callcandidateseventtest.cpp deleted file mode 100644 index ca908db3..00000000 --- a/tests/callcandidateseventtest.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "events/callcandidatesevent.h" -#include "callcandidateseventtest.h" - -void TestCallCandidatesEvent::fromJson() -{ - auto document = QJsonDocument::fromJson(R"({ - "age": 242352, - "content": { - "call_id": "12345", - "candidates": [ - { - "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0", - "sdpMLineIndex": 0, - "sdpMid": "audio" - } - ], - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.candidates" - })"); - - QVERIFY(document.isObject()); - - auto object = documemt.object(); - - Quotient::CallCandidatesEvent callCandidatesEvent(object); - - QCOMPARE(callCandidatesEvent.version(), 0); - QCOMPARE(callCandidatesEvent.callId(), "12345"); - QCOMPARE(callCandidatesEvent.candidates().count(), 1); - - const QJsonObject &candidate = callCandidatesEvent.candidates().at(0).toObject(); - QCOMPARE(candidate.value("sdpMid").toString(), "audio"); - QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0); - QCOMPARE(candidate.value("candidate").toString(), "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0"); -} - -QTEST_MAIN(TestCallCandidatesEvent) -#include "callcandidateseventtest.moc" diff --git a/tests/callcandidateseventtest.h b/tests/callcandidateseventtest.h deleted file mode 100644 index b81c9c9b..00000000 --- a/tests/callcandidateseventtest.h +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include - -class TestCallCandidatesEvent : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void fromJson(); -}; diff --git a/tests/quotest.cpp b/tests/quotest.cpp deleted file mode 100644 index 5098bc02..00000000 --- a/tests/quotest.cpp +++ /dev/null @@ -1,864 +0,0 @@ -// SPDX-FileCopyrightText: 2016 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "connection.h" -#include "room.h" -#include "user.h" -#include "uriresolver.h" - -#include "csapi/joining.h" -#include "csapi/leaving.h" -#include "csapi/room_send.h" - -#include "events/reactionevent.h" -#include "events/redactionevent.h" -#include "events/simplestateevents.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -using namespace Quotient; -using std::clog, std::endl; - -class TestSuite; - -class TestManager : public QCoreApplication { -public: - TestManager(int& argc, char** argv); - -private: - void setupAndRun(); - void onNewRoom(Room* r); - void doTests(); - void conclude(); - void finalize(); - -private: - Connection* c = nullptr; - QString origin; - QString targetRoomName; - TestSuite* testSuite = nullptr; - QByteArrayList running {}, succeeded {}, failed {}; -}; - -using TestToken = QByteArray; // return value of QMetaMethod::name -Q_DECLARE_METATYPE(TestToken) - -// For now, the token itself is the test name but that may change. -const char* testName(const TestToken& token) { return token.constData(); } - -/// Test function declaration -/*! - * \return true, if the test finished (successfully or unsuccessfully); - * false, if the test went async and will complete later - */ -#define TEST_DECL(Name) bool Name(const TestToken& thisTest); - -/// The holder for the actual tests -/*! - * This class takes inspiration from Qt Test in terms of tests invocation; - * TestManager instantiates it and runs all public slots (cf. private slots in - * Qt Test) one after another. An important diversion from Qt Test is that - * the tests are assumed to by asynchronous rather than synchronous; so it's - * perfectly normal to have a few tests running at the same time. To avoid - * context clashes a special parameter with the name thisTest is passed to - * each test. Each test must conclude (synchronously or asynchronously) with - * an invocation of FINISH_TEST() macro (or FAIL_TEST() macro that expands to - * FINISH_TEST) that expects thisTest variable to be reachable. If FINISH_TEST() - * is invoked twice with the same thisTest, the second call will cause assertion - * failure; if FINISH_TEST() is not invoked at all, the test will be killed - * by a watchdog after a timeout and marked in the final report as not finished. - */ -class TestSuite : public QObject { - Q_OBJECT -public: - TestSuite(Room* testRoom, QString source, TestManager* parent) - : QObject(parent), targetRoom(testRoom), origin(std::move(source)) - { - qRegisterMetaType(); - Q_ASSERT(testRoom && parent); - } - -signals: - void finishedItem(QByteArray /*name*/, bool /*condition*/); - -public slots: - void doTest(const QByteArray& testName); - -private slots: - TEST_DECL(findRoomByAlias) - TEST_DECL(loadMembers) - TEST_DECL(sendMessage) - TEST_DECL(sendReaction) - TEST_DECL(sendFile) - TEST_DECL(setTopic) - TEST_DECL(changeName) - TEST_DECL(sendAndRedact) - TEST_DECL(addAndRemoveTag) - TEST_DECL(markDirectChat) - TEST_DECL(visitResources) - // Add more tests above here - -public: - [[nodiscard]] Room* room() const { return targetRoom; } - [[nodiscard]] Connection* connection() const - { - return targetRoom->connection(); - } - -private: - [[nodiscard]] bool checkFileSendingOutcome(const TestToken& thisTest, - const QString& txnId, - const QString& fileName); - [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, - const QString& evtIdToRedact); - - [[nodiscard]] bool validatePendingEvent(const QString& txnId); - [[nodiscard]] bool checkDirectChat() const; - void finishTest(const TestToken& token, bool condition, const char* file, - int line); - -private: - Room* targetRoom; - QString origin; -}; - -#define TEST_IMPL(Name) bool TestSuite::Name(const TestToken& thisTest) - -// Returning true (rather than a void) allows to reuse the convention with -// connectUntil() to break the QMetaObject::Connection upon finishing the test -// item. -#define FINISH_TEST(Condition) \ - return (finishTest(thisTest, Condition, __FILE__, __LINE__), true) - -#define FAIL_TEST() FINISH_TEST(false) - -void TestSuite::doTest(const QByteArray& testName) -{ - clog << "Starting: " << testName.constData() << endl; - QMetaObject::invokeMethod(this, testName, Qt::DirectConnection, - Q_ARG(TestToken, testName)); -} - -bool TestSuite::validatePendingEvent(const QString& txnId) -{ - auto it = targetRoom->findPendingEvent(txnId); - return it != targetRoom->pendingEvents().end() - && it->deliveryStatus() == EventStatus::Submitted - && (*it)->transactionId() == txnId; -} - -void TestSuite::finishTest(const TestToken& token, bool condition, - const char* file, int line) -{ - const auto& item = testName(token); - if (condition) { - clog << item << " successful" << endl; - if (targetRoom) - targetRoom->postMessage(origin % ": " % item % " successful", - MessageEventType::Notice); - } else { - clog << item << " FAILED at " << file << ":" << line << endl; - if (targetRoom) - targetRoom->postPlainText(origin % ": " % item % " FAILED at " - % file % ", line " % QString::number(line)); - } - - emit finishedItem(item, condition); -} - -TestManager::TestManager(int& argc, char** argv) - : QCoreApplication(argc, argv), c(new Connection(this)) -{ - Q_ASSERT(argc >= 5); - clog << "Connecting to Matrix as " << argv[1] << endl; - c->loginWithPassword(argv[1], argv[2], argv[3]); - targetRoomName = argv[4]; - clog << "Test room name: " << argv[4] << endl; - if (argc > 5) { - origin = argv[5]; - clog << "Origin for the test message: " << origin.toStdString() << endl; - } - - connect(c, &Connection::connected, this, &TestManager::setupAndRun); - connect(c, &Connection::resolveError, this, - [](const QString& error) { - clog << "Failed to resolve the server: " << error.toStdString() - << endl; - QCoreApplication::exit(-2); - }, - Qt::QueuedConnection); - connect(c, &Connection::loginError, this, - [this](const QString& message, const QString& details) { - clog << "Failed to login to " - << c->homeserver().toDisplayString().toStdString() << ": " - << message.toStdString() << endl - << "Details:" << endl - << details.toStdString() << endl; - QCoreApplication::exit(-2); - }, - Qt::QueuedConnection); - connect(c, &Connection::loadedRoomState, this, &TestManager::onNewRoom); - - // Big countdown watchdog - QTimer::singleShot(180000, this, [this] { - if (testSuite) - conclude(); - else - finalize(); - }); -} - -void TestManager::setupAndRun() -{ - Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); - Q_ASSERT(c->domain() == c->userId().section(':', 1)); - clog << "Connected, server: " - << c->homeserver().toDisplayString().toStdString() << endl; - clog << "Access token: " << c->accessToken().toStdString() << endl; - - c->setLazyLoading(true); - - clog << "Joining " << targetRoomName.toStdString() << endl; - auto joinJob = c->joinRoom(targetRoomName); - // Ensure that the room has been joined and filled with some events - // so that other tests could use that - connect(joinJob, &BaseJob::success, this, [this, joinJob] { - testSuite = new TestSuite(c->room(joinJob->roomId()), origin, this); - // Only start the sync after joining, to make sure the room just - // joined is in it - c->syncLoop(); - connect(c, &Connection::syncDone, this, [this] { - static int i = 0; - clog << "Sync " << ++i << " complete" << endl; - if (auto* r = testSuite->room()) { - clog << "Test room timeline size = " << r->timelineSize(); - if (r->pendingEvents().empty()) - clog << ", pending size = " << r->pendingEvents().size(); - clog << endl; - } - if (!running.empty()) { - clog << running.size() << " test(s) in the air:"; - for (const auto& test: qAsConst(running)) - clog << " " << testName(test); - clog << endl; - } - if (i == 1) { - testSuite->room()->getPreviousContent(); - connectSingleShot(testSuite->room(), &Room::addedMessages, this, - &TestManager::doTests); - } - }); - }); - connect(joinJob, &BaseJob::failure, this, [this] { - clog << "Failed to join the test room" << endl; - finalize(); - }); -} - -void TestManager::onNewRoom(Room* r) -{ - clog << "New room: " << r->id().toStdString() << endl - << " Name: " << r->name().toStdString() << endl - << " Canonical alias: " << r->canonicalAlias().toStdString() << endl - << endl; - connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { - clog << timeline.size() << " new event(s) in room " - << r->objectName().toStdString() << endl; - }); -} - -void TestManager::doTests() -{ - const auto* metaObj = testSuite->metaObject(); - for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); ++i) { - const auto metaMethod = metaObj->method(i); - if (metaMethod.access() != QMetaMethod::Private - || metaMethod.methodType() != QMetaMethod::Slot) - continue; - - const auto testName = metaMethod.name(); - running.push_back(testName); - // Some tests return the result immediately but we queue everything - // and process all tests asynchronously. - QMetaObject::invokeMethod(testSuite, "doTest", Qt::QueuedConnection, - Q_ARG(QByteArray, testName)); - } - clog << "Tests to do:"; - for (const auto& test: qAsConst(running)) - clog << " " << testName(test); - clog << endl; - connect(testSuite, &TestSuite::finishedItem, this, - [this](const QByteArray& itemName, bool condition) { - if (auto i = running.indexOf(itemName); i != -1) - (condition ? succeeded : failed).push_back(running.takeAt(i)); - else - Q_ASSERT_X(false, itemName, - "Test item is not in running state"); - if (running.empty()) { - clog << "All tests finished" << endl; - conclude(); - } - }); -} - -TEST_IMPL(findRoomByAlias) -{ - auto* roomByAlias = connection()->roomByAlias(targetRoom->canonicalAlias(), - JoinState::Join); - FINISH_TEST(roomByAlias == targetRoom); -} - -TEST_IMPL(loadMembers) -{ - // It's not exactly correct because an arbitrary server might not support - // lazy loading; but in the absence of capabilities framework we assume - // it does. - if (targetRoom->users().size() >= targetRoom->joinedCount()) { - clog << "Lazy loading doesn't seem to be enabled" << endl; - FAIL_TEST(); - } - targetRoom->setDisplayed(); - connect(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { - FINISH_TEST(targetRoom->users().size() >= targetRoom->joinedCount()); - }); - return false; -} - -TEST_IMPL(sendMessage) -{ - auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - if (!validatePendingEvent(txnId)) { - clog << "Invalid pending event right after submitting" << endl; - FAIL_TEST(); - } - connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, - [this, thisTest, txnId](const RoomEvent* evt, int pendingIdx) { - const auto& pendingEvents = targetRoom->pendingEvents(); - Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); - - if (evt->transactionId() != txnId) - return false; - - FINISH_TEST(is(*evt) && !evt->id().isEmpty() - && pendingEvents[size_t(pendingIdx)]->transactionId() - == evt->transactionId()); - }); - return false; -} - -TEST_IMPL(sendReaction) -{ - clog << "Reacting to the newest message in the room" << endl; - Q_ASSERT(targetRoom->timelineSize() > 0); - const auto targetEvtId = targetRoom->messageEvents().back()->id(); - const auto key = QStringLiteral("+1"); - const auto txnId = targetRoom->postReaction(targetEvtId, key); - if (!validatePendingEvent(txnId)) { - clog << "Invalid pending event right after submitting" << endl; - FAIL_TEST(); - } - - connectUntil(targetRoom, &Room::updatedEvent, this, - [this, thisTest, txnId, key, targetEvtId](const QString& actualTargetEvtId) { - if (actualTargetEvtId != targetEvtId) - return false; - const auto reactions = targetRoom->relatedEvents( - targetEvtId, EventRelation::Annotation()); - // It's a test room, assuming no interference there should - // be exactly one reaction - if (reactions.size() != 1) - FAIL_TEST(); - - const auto* evt = - eventCast(reactions.back()); - FINISH_TEST(is(*evt) && !evt->id().isEmpty() - && evt->relation().key == key - && evt->transactionId() == txnId); - // TODO: Test removing the reaction - }); - return false; -} - -TEST_IMPL(sendFile) -{ - auto* tf = new QTemporaryFile; - if (!tf->open()) { - clog << "Failed to create a temporary file" << endl; - FAIL_TEST(); - } - tf->write("Test"); - tf->close(); - // QFileInfo::fileName brings only the file name; QFile::fileName brings - // the full path - const auto tfName = QFileInfo(*tf).fileName(); - clog << "Sending file " << tfName.toStdString() << endl; - const auto txnId = - targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName())); - if (!validatePendingEvent(txnId)) { - clog << "Invalid pending event right after submitting" << endl; - delete tf; - FAIL_TEST(); - } - - // Using tf as a context object to clean away both connections - // once either of them triggers. - connectUntil(targetRoom, &Room::fileTransferCompleted, tf, - [this, thisTest, txnId, tf, tfName](const QString& id) { - auto fti = targetRoom->fileTransferInfo(id); - Q_ASSERT(fti.status == FileTransferInfo::Completed); - - if (id != txnId) - return false; - - tf->deleteLater(); - return checkFileSendingOutcome(thisTest, txnId, tfName); - }); - connectUntil(targetRoom, &Room::fileTransferFailed, tf, - [this, thisTest, txnId, tf](const QString& id, const QString& error) { - if (id != txnId) - return false; - - targetRoom->postPlainText(origin % ": File upload failed: " % error); - tf->deleteLater(); - FAIL_TEST(); - }); - return false; -} - -bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, - const QString& txnId, - const QString& fileName) -{ - auto it = targetRoom->findPendingEvent(txnId); - if (it == targetRoom->pendingEvents().end()) { - clog << "Pending file event dropped before upload completion" << endl; - FAIL_TEST(); - } - if (it->deliveryStatus() != EventStatus::FileUploaded) { - clog << "Pending file event status upon upload completion is " - << it->deliveryStatus() << " != FileUploaded(" - << EventStatus::FileUploaded << ')' << endl; - FAIL_TEST(); - } - - connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, - [this, thisTest, txnId, fileName](const RoomEvent* evt, int pendingIdx) { - const auto& pendingEvents = targetRoom->pendingEvents(); - Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); - - if (evt->transactionId() != txnId) - return false; - - clog << "File event " << txnId.toStdString() - << " arrived in the timeline" << endl; - // This part tests visit() - return visit( - *evt, - [&](const RoomMessageEvent& e) { - // TODO: actually try to download it to check, e.g., #366 - // (and #368 would help to test against bad file names). - FINISH_TEST( - !e.id().isEmpty() - && pendingEvents[size_t(pendingIdx)]->transactionId() - == txnId - && e.hasFileContent() - && e.content()->fileInfo()->originalName == fileName); - }, - [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); - }); - return true; -} - -TEST_IMPL(setTopic) -{ - const auto newTopic = connection()->generateTxnId(); // Just a way to make - // a unique id - targetRoom->setTopic(newTopic); - connectUntil(targetRoom, &Room::topicChanged, this, - [this, thisTest, newTopic] { - if (targetRoom->topic() == newTopic) - FINISH_TEST(true); - - clog << "Requested topic was " << newTopic.toStdString() << ", " - << targetRoom->topic().toStdString() << " arrived instead" - << endl; - return false; - }); - return false; -} - -TEST_IMPL(changeName) -{ - auto* const localUser = connection()->user(); - const auto& newName = connection()->generateTxnId(); // See setTopic() - clog << "Renaming the user to " << newName.toStdString() << endl; - localUser->rename(newName); - connectUntil(localUser, &User::defaultNameChanged, this, - [this, thisTest, localUser, newName] { - FINISH_TEST(localUser->name() == newName); - }); - return false; -} - -TEST_IMPL(sendAndRedact) -{ - clog << "Sending a message to redact" << endl; - auto txnId = targetRoom->postPlainText(origin % ": message to redact"); - if (txnId.isEmpty()) - FAIL_TEST(); - - connectUntil(targetRoom, &Room::messageSent, this, - [this, thisTest, txnId](const QString& tId, const QString& evtId) { - if (tId != txnId) - return false; - - // The event may end up having been merged, and that's ok; - // but if it's not, it has to be in the ReachedServer state. - if (auto it = room()->findPendingEvent(tId); - it != room()->pendingEvents().cend() - && it->deliveryStatus() != EventStatus::ReachedServer) { - clog << "Incorrect sent event status (" - << it->deliveryStatus() << ')' << endl; - FAIL_TEST(); - } - - clog << "Redacting the message" << endl; - targetRoom->redactEvent(evtId, origin); - connectUntil(targetRoom, &Room::addedMessages, this, - [this, thisTest, evtId] { - return checkRedactionOutcome(thisTest, evtId); - }); - return false; - }); - return false; -} - -bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest, - const QString& evtIdToRedact) -{ - // There are two possible (correct) outcomes: either the event comes already - // redacted at the next sync, or the nearest sync completes with - // the unredacted event but the next one brings redaction. - auto it = targetRoom->findInTimeline(evtIdToRedact); - if (it == targetRoom->timelineEdge()) - return false; // Waiting for the next sync - - if ((*it)->isRedacted()) { - clog << "The sync brought already redacted message" << endl; - FINISH_TEST(true); - } - - clog << "Message came non-redacted with the sync, waiting for redaction" - << endl; - connectUntil(targetRoom, &Room::replacedEvent, this, - [this, thisTest, evtIdToRedact](const RoomEvent* newEvent, - const RoomEvent* oldEvent) { - if (oldEvent->id() != evtIdToRedact) - return false; - - FINISH_TEST(newEvent->isRedacted() - && newEvent->redactionReason() == origin); - }); - return true; -} - -TEST_IMPL(addAndRemoveTag) -{ - static const auto TestTag = QStringLiteral("im.quotient.test"); - // Pre-requisite - if (targetRoom->tags().contains(TestTag)) - targetRoom->removeTag(TestTag); - - // Unlike for most of Quotient, tags are applied and tagsChanged is emitted - // synchronously, with the server being notified async. The test checks - // that the signal is emitted, not only that tags have changed; but there's - // (currently) no way to check that the server has been correctly notified - // of the tag change. - QSignalSpy spy(targetRoom, &Room::tagsChanged); - targetRoom->addTag(TestTag); - if (spy.count() != 1 || !targetRoom->tags().contains(TestTag)) { - clog << "Tag adding failed" << endl; - FAIL_TEST(); - } - clog << "Test tag set, removing it now" << endl; - targetRoom->removeTag(TestTag); - FINISH_TEST(spy.count() == 2 && !targetRoom->tags().contains(TestTag)); -} - -bool TestSuite::checkDirectChat() const -{ - return targetRoom->directChatUsers().contains(connection()->user()); -} - -TEST_IMPL(markDirectChat) -{ - if (checkDirectChat()) - connection()->removeFromDirectChats(targetRoom->id(), - connection()->user()); - - int id = qRegisterMetaType(); // For QSignalSpy - Q_ASSERT(id != -1); - - // Same as with tags (and unusual for the rest of Quotient), direct chat - // operations are synchronous. - QSignalSpy spy(connection(), &Connection::directChatsListChanged); - clog << "Marking the room as a direct chat" << endl; - connection()->addToDirectChats(targetRoom, connection()->user()); - if (spy.count() != 1 || !checkDirectChat()) - FAIL_TEST(); - - // Check that the first argument (added DCs) actually contains the room - const auto& addedDCs = spy.back().front().value(); - if (addedDCs.size() != 1 - || !addedDCs.contains(connection()->user(), targetRoom->id())) { - clog << "The room is not in added direct chats" << endl; - FAIL_TEST(); - } - - clog << "Unmarking the direct chat" << endl; - connection()->removeFromDirectChats(targetRoom->id(), connection()->user()); - if (spy.count() != 2 && checkDirectChat()) - FAIL_TEST(); - - // Check that the second argument (removed DCs) actually contains the room - const auto& removedDCs = spy.back().back().value(); - FINISH_TEST(removedDCs.size() == 1 - && removedDCs.contains(connection()->user(), targetRoom->id())); -} - -TEST_IMPL(visitResources) -{ - // Same as the two tests above, ResourceResolver emits signals - // synchronously so we use signal spies to intercept them instead of - // connecting lambdas before calling openResource(). NB: this test - // assumes that ResourceResolver::openResource is implemented in terms - // of ResourceResolver::visitResource, so the latter doesn't need a - // separate test. - static UriDispatcher ud; - - // This lambda returns true in case of error, false if it's fine so far - auto testResourceResolver = [this, thisTest](const QStringList& uris, - auto signal, auto* target, - QVariantList otherArgs = {}) { - int r = qRegisterMetaType(); - Q_ASSERT(r != 0); - QSignalSpy spy(&ud, signal); - for (const auto& uriString: uris) { - Uri uri { uriString }; - clog << "Checking " << uriString.toStdString() - << " -> " << uri.toDisplayString().toStdString() << endl; - if (auto matrixToUrl = uri.toUrl(Uri::MatrixToUri).toDisplayString(); - !matrixToUrl.startsWith("https://matrix.to/#/")) { - clog << "Incorrect matrix.to representation:" - << matrixToUrl.toStdString() << endl; - } - ud.visitResource(connection(), uriString); - if (spy.count() != 1) { - clog << "Wrong number of signal emissions (" << spy.count() - << ')' << endl; - FAIL_TEST(); - } - const auto& emission = spy.front(); - Q_ASSERT(emission.count() >= 2); - if (emission.front().value() != target) { - clog << "Signal emitted with an incorrect target" << endl; - FAIL_TEST(); - } - if (!otherArgs.empty()) { - if (emission.size() < otherArgs.size() + 1) { - clog << "Emission doesn't include all arguments" << endl; - FAIL_TEST(); - } - for (auto i = 0; i < otherArgs.size(); ++i) - if (otherArgs[i] != emission[i + 1]) { - clog << "Mismatch in argument #" << i + 1 << endl; - FAIL_TEST(); - } - } - spy.clear(); - } - return false; - }; - - // Basic tests - for (const auto& u: { Uri {}, Uri { QUrl {} } }) - if (u.isValid() || !u.isEmpty()) { - clog << "Empty Matrix URI test failed" << endl; - FAIL_TEST(); - } - if (Uri { QStringLiteral("#") }.isValid()) { - clog << "Bare sigil URI test failed" << endl; - FAIL_TEST(); - } - QUrl invalidUrl { "https://" }; - invalidUrl.setAuthority("---:@@@"); - const Uri matrixUriFromInvalidUrl { invalidUrl }, - invalidMatrixUri { QStringLiteral("matrix:&invalid@") }; - if (matrixUriFromInvalidUrl.isEmpty() || matrixUriFromInvalidUrl.isValid()) { - clog << "Invalid Matrix URI test failed" << endl; - FAIL_TEST(); - } - if (invalidMatrixUri.isEmpty() || invalidMatrixUri.isValid()) { - clog << "Invalid sigil in a Matrix URI - test failed" << endl; - FAIL_TEST(); - } - - // Matrix identifiers used throughout all URI tests - const auto& roomId = room()->id(); - const auto& roomAlias = room()->canonicalAlias(); - const auto& userId = connection()->userId(); - const auto& eventId = room()->messageEvents().back()->id(); - Q_ASSERT(!roomId.isEmpty()); - Q_ASSERT(!roomAlias.isEmpty()); - Q_ASSERT(!userId.isEmpty()); - Q_ASSERT(!eventId.isEmpty()); - - const QStringList roomUris { - roomId, "matrix:roomid/" + roomId.mid(1), - "https://matrix.to/#/%21"/*`!`*/ + roomId.mid(1), - roomAlias, "matrix:room/" + roomAlias.mid(1), - "https://matrix.to/#/" + roomAlias, - }; - const QStringList userUris { userId, "matrix:user/" + userId.mid(1), - "https://matrix.to/#/" + userId }; - const QStringList eventUris { - "matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1), - "https://matrix.to/#/" + roomId + '/' + eventId - }; - // Check that reserved characters are correctly processed. - static const auto& joinRoomAlias = - QStringLiteral("##/?.@\"unjoined:example.org"); - static const auto& encodedRoomAliasNoSigil = - QUrl::toPercentEncoding(joinRoomAlias.mid(1), ":"); - static const QString joinQuery { "?action=join" }; - // These URIs are not supposed to be actually joined (and even exist, - // as yet) - only to be syntactically correct - static const QStringList joinByAliasUris { - Uri(joinRoomAlias.toUtf8(), {}, joinQuery.mid(1)).toDisplayString(), - "matrix:room/" + encodedRoomAliasNoSigil + joinQuery, - "https://matrix.to/#/%23"/*`#`*/ + encodedRoomAliasNoSigil + joinQuery, - "https://matrix.to/#/%23" + joinRoomAlias.mid(1) /* unencoded */ + joinQuery - }; - static const auto& joinRoomId = QStringLiteral("!anyid:example.org"); - static const QStringList viaServers { "matrix.org", "example.org" }; - static const auto viaQuery = - std::accumulate(viaServers.cbegin(), viaServers.cend(), joinQuery, - [](const QString& q, const QString& s) { - return q + "&via=" + s; - }); - static const QStringList joinByIdUris { - "matrix:roomid/" + joinRoomId.mid(1) + viaQuery, - "https://matrix.to/#/" + joinRoomId + viaQuery - }; - // If any test breaks, the breaking call will return true, and further - // execution will be cut by ||'s short-circuiting - if (testResourceResolver(roomUris, &UriDispatcher::roomAction, room()) - || testResourceResolver(userUris, &UriDispatcher::userAction, - connection()->user()) - || testResourceResolver(eventUris, &UriDispatcher::roomAction, - room(), { eventId }) - || testResourceResolver(joinByAliasUris, &UriDispatcher::joinAction, - connection(), { joinRoomAlias }) - || testResourceResolver(joinByIdUris, &UriDispatcher::joinAction, - connection(), { joinRoomId, viaServers })) - return true; - // TODO: negative cases - FINISH_TEST(true); -} - -void TestManager::conclude() -{ - // Clean up the room (best effort) - auto* room = testSuite->room(); - room->setTopic({}); - room->localUser()->rename({}); - - QString succeededRec { QString::number(succeeded.size()) % " of " - % QString::number(succeeded.size() + failed.size() - + running.size()) - % " tests succeeded" }; - QString plainReport = origin % ": Testing complete, " % succeededRec; - QString color = failed.empty() && running.empty() ? "00AA00" : "AA0000"; - QString htmlReport = origin % ": Testing complete, " % succeededRec; - if (!failed.empty()) { - QByteArray failedList; - for (const auto& f : qAsConst(failed)) - failedList += ' ' + f; - plainReport += "\nFAILED:" + failedList; - htmlReport += "
Failed:" + failedList; - } - if (!running.empty()) { - QByteArray dnfList; - for (const auto& r : qAsConst(running)) - dnfList += ' ' + r; - plainReport += "\nDID NOT FINISH:" + dnfList; - htmlReport += "
Did not finish:" + dnfList; - } - - auto txnId = room->postHtmlText(plainReport, htmlReport); - // Now just wait until all the pending events reach the server - connectUntil(room, &Room::messageSent, this, - [this, txnId, room, plainReport] (const QString& sentTxnId) { - if (sentTxnId != txnId) - return false; - const auto& pendingEvents = room->pendingEvents(); - if (auto c = std::count_if(pendingEvents.cbegin(), - pendingEvents.cend(), - [](const PendingEventItem& pe) { - return pe.deliveryStatus() - < EventStatus::ReachedServer; - }); - c > 0) { - clog << "Events to reach the server: " << c - << ", not leaving yet" << endl; - return false; - } - - clog << "Leaving the room" << endl; - // TODO: Waiting for proper futures to come so that it could be: -// room->leaveRoom() -// .then(this, &TestManager::finalize); // Qt-style or -// .then([this] { finalize(); }); // STL-style - auto* job = room->leaveRoom(); - connect(job, &BaseJob::result, this, [this, job,plainReport] { - Q_ASSERT(job->status().good()); - finalize(); - // Still flying, as the exit() connection in finalize() is queued - clog << plainReport.toStdString() << endl; - }); - return true; - }); -} - -void TestManager::finalize() -{ - clog << "Logging out" << endl; - c->logout(); - connect(c, &Connection::loggedOut, this, - [this] { QCoreApplication::exit(failed.size() + running.size()); }, - Qt::QueuedConnection); -} - -int main(int argc, char* argv[]) -{ - // TODO: use QCommandLineParser - if (argc < 5) { - clog << "Usage: quotest [origin]" - << endl; - return -1; - } - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - return TestManager(argc, argv).exec(); -} - -#include "quotest.moc" -- cgit v1.2.3 From ba9c361b6ee18401fcd9a79175f56758a11d7f7d Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 21 Jan 2021 19:21:00 +0100 Subject: fix build --- autotests/CMakeLists.txt | 2 +- autotests/callcandidateseventtest.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index abfcb49a..07f1f046 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -11,4 +11,4 @@ function(QUOTIENT_ADD_TEST) add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME}) endfunction() -quotient_add_test(callcandidateseventtest.cpp NAME callcandidateseventtest) +quotient_add_test(NAME callcandidateseventtest) diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index ca908db3..f103e4d3 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -29,7 +29,7 @@ void TestCallCandidatesEvent::fromJson() QVERIFY(document.isObject()); - auto object = documemt.object(); + auto object = document.object(); Quotient::CallCandidatesEvent callCandidatesEvent(object); -- cgit v1.2.3 From 390162a0c707c51590acb27df81e98a85d3b6cf7 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 22 Jan 2021 15:40:52 +0100 Subject: Remove quotest from ctest --- quotest/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index 4553abb6..d17e8620 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -5,5 +5,4 @@ set(quotest_SRCS quotest.cpp) add_executable(quotest ${quotest_SRCS}) -add_test(NAME quotest COMMAND quotest) target_link_libraries(quotest PRIVATE Qt5::Core Qt5::Test ${PROJECT_NAME}) -- cgit v1.2.3 From ced197fc6606c0f12eee161408742da54f40411b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 22 Jan 2021 17:30:20 +0100 Subject: Adjust according to the just merged PR --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 201a4186..d121e432 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,13 +102,13 @@ jobs: - name: Build tests run: | - cmake tests -Bbuild-test $CMAKE_ARGS - cmake --build build-test --target all + cmake -S quotest -Bbuild-quotest $CMAKE_ARGS + cmake --build build-quotest --target all - name: Run tests env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} run: | - [[ -z "$TEST_USER" ]] || $VALGRIND build-test/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + [[ -z "$TEST_USER" ]] || $VALGRIND build-quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From 3a23e40be271a54db84c39c1ba47956915bb23ec Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 21 Jan 2021 21:30:31 +0100 Subject: Load user info (display name + avatar) for the local user. This is needed for a few cases like the account list in NeoChat or the account switcher. In this cases we don't have a room binded to the user and can't fetch the real display name and avatar. --- lib/user.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/user.cpp b/lib/user.cpp index bed7be3a..9f0386f4 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -49,6 +49,16 @@ User::User(QString userId, Connection* connection) : QObject(connection), d(new Private(move(userId))) { setObjectName(id()); + if (connection->userId() == id()) { + // Load profile information for local user. + auto *profileJob = connection->callApi(id()); + connect(profileJob, &BaseJob::result, this, [this, profileJob] { + d->defaultName = profileJob->displayname(); + d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); + emit defaultNameChanged(); + emit defaultAvatarChanged(); + }); + } } Connection* User::connection() const -- cgit v1.2.3 From 680e0f9deb3042db5cda43b81dbc7b1c153b8744 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 23 Jan 2021 16:33:43 +0100 Subject: Add test --- quotest/quotest.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 5098bc02..f693319f 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -101,6 +101,7 @@ private slots: TEST_DECL(setTopic) TEST_DECL(changeName) TEST_DECL(sendAndRedact) + TEST_DECL(showLocalUsername) TEST_DECL(addAndRemoveTag) TEST_DECL(markDirectChat) TEST_DECL(visitResources) @@ -508,6 +509,17 @@ TEST_IMPL(changeName) return false; } + +TEST_IMPL(showLocalUsername) +{ + auto* const localUser = connection()->user(); + if (localUser->name().contains("@")) { + // it is using the id fallback :( + FAIL_TEST(); + } + return false; +} + TEST_IMPL(sendAndRedact) { clog << "Sending a message to redact" << endl; -- cgit v1.2.3 From bfaf9330c4214636478df977ab7f6b5d1d843104 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 23 Jan 2021 18:14:46 +0100 Subject: Update test --- quotest/quotest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index f693319f..ed1c1d13 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -517,6 +517,7 @@ TEST_IMPL(showLocalUsername) // it is using the id fallback :( FAIL_TEST(); } + FINISH_TEST(true); return false; } -- cgit v1.2.3 From 4920adbc13db417756a0ee4c033fad96b8249eea Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 09:06:31 +0100 Subject: CMakeLists.txt: include(CTest) It's a missing line from the recent autotests introduction. Without it CMake doesn't introduce BUILD_TESTING option, and if you explicitly pass it, fails to configure the project. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bc3a559..a458c9ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ message(STATUS) message(STATUS "Configuring ${PROJECT_NAME} ${PROJECT_VERSION} ==>") include(FeatureSummary) +include(CTest) option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS -- cgit v1.2.3 From 08de0609df2649170ecdc927c312e08387e6cc67 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 09:14:14 +0100 Subject: CI: no need to explicitly build quotest Now that CTest is included, quotest target should build (and install) automatically together with the library. --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d121e432..7077ff35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,15 +100,10 @@ jobs: - name: Build and install libQuotient run: cmake --build build --target install - - name: Build tests - run: | - cmake -S quotest -Bbuild-quotest $CMAKE_ARGS - cmake --build build-quotest --target all - - name: Run tests env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} run: | - [[ -z "$TEST_USER" ]] || $VALGRIND build-quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From a8e8f1cd39e846941de544c7791e1ab6398a335b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 10:33:25 +0100 Subject: Collapse the autotest entirely in *.cpp file Otherwise CMake's automoc complains that it doesn't see a moc-able class definition in the file that includes "*.moc". --- autotests/callcandidateseventtest.cpp | 11 ++++++++++- autotests/callcandidateseventtest.h | 13 ------------- 2 files changed, 10 insertions(+), 14 deletions(-) delete mode 100644 autotests/callcandidateseventtest.h diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index f103e4d3..39e6a116 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -3,7 +3,16 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "events/callcandidatesevent.h" -#include "callcandidateseventtest.h" + +#include + +class TestCallCandidatesEvent : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void fromJson(); +}; void TestCallCandidatesEvent::fromJson() { diff --git a/autotests/callcandidateseventtest.h b/autotests/callcandidateseventtest.h deleted file mode 100644 index b81c9c9b..00000000 --- a/autotests/callcandidateseventtest.h +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include - -class TestCallCandidatesEvent : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void fromJson(); -}; -- cgit v1.2.3 From f621367f0706ae3093eedb8cf0d068c34acee6ea Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 10:35:39 +0100 Subject: Autotests: make sure types passed to QCOMPARE are the same The current test may fail with "undefined reference" errors discussed at https://stackoverflow.com/questions/14198972/undefined-symbols-for-qcompare. --- autotests/callcandidateseventtest.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index 39e6a116..1a69cb66 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -43,13 +43,14 @@ void TestCallCandidatesEvent::fromJson() Quotient::CallCandidatesEvent callCandidatesEvent(object); QCOMPARE(callCandidatesEvent.version(), 0); - QCOMPARE(callCandidatesEvent.callId(), "12345"); + QCOMPARE(callCandidatesEvent.callId(), QStringLiteral("12345")); QCOMPARE(callCandidatesEvent.candidates().count(), 1); const QJsonObject &candidate = callCandidatesEvent.candidates().at(0).toObject(); - QCOMPARE(candidate.value("sdpMid").toString(), "audio"); + QCOMPARE(candidate.value("sdpMid").toString(), QStringLiteral("audio")); QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0); - QCOMPARE(candidate.value("candidate").toString(), "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0"); + QCOMPARE(candidate.value("candidate").toString(), + QStringLiteral("candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0")); } QTEST_MAIN(TestCallCandidatesEvent) -- cgit v1.2.3 From a1b1b96bcd78c6efb0ad3c1ba9c93284497d74a6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 10:38:02 +0100 Subject: CMakeLists.txt: refactor configuration of features - The feature summary is only generated at the end of the configuration. - InstallQuotest feature is defined in quotest/CMakeLists.txt now, and therefore is only available if quotest is getting built (i.e., if BUILD_TESTING is on). - API generation configuration code merged from two places into one. --- CMakeLists.txt | 78 +++++++++++++++++++++++--------------------------- quotest/CMakeLists.txt | 8 ++++++ 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a458c9ff..39b1b03a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,10 +12,6 @@ message(STATUS "Configuring ${PROJECT_NAME} ${PROJECT_VERSION} ==>") include(FeatureSummary) include(CTest) -option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) -add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS - "the library functional test suite") - # https://github.com/quotient-im/libQuotient/issues/369 option(${PROJECT_NAME}_ENABLE_E2EE "end-to-end encryption (E2EE) support" OFF) add_feature_info(EnableE2EE ${PROJECT_NAME}_ENABLE_E2EE @@ -115,36 +111,6 @@ if (${PROJECT_NAME}_ENABLE_E2EE) endif () endif () -if (GTAD_PATH) - get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) -endif () -if (MATRIX_DOC_PATH) - get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) -endif () -if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) - message( STATUS "Using GTAD at ${ABS_GTAD_PATH}" ) - message( STATUS "Using API files at ${ABS_API_DEF_PATH}" ) - set(API_GENERATION_ENABLED 1) - if (NOT CLANG_FORMAT) - set(CLANG_FORMAT clang-format) - endif() - get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM) - if (ABS_CLANG_FORMAT) - set(API_FORMATTING_ENABLED 1) - message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") - else () - message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") - endif () -endif() -add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" - "build target update-api") -add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" - "formatting of generated API files with clang-format") - -message(STATUS) -feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES - FATAL_ON_MISSING_REQUIRED_PACKAGES) - # Set up source files set(lib_SRCS lib/networkaccessmanager.cpp @@ -192,12 +158,34 @@ set(lib_SRCS lib/jobs/downloadfilejob.cpp ) +# Configure API files generation + set(CSAPI_DIR csapi) set(FULL_CSAPI_DIR lib/${CSAPI_DIR}) set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) -if (MATRIX_DOC_PATH AND GTAD_PATH) +if (GTAD_PATH) + get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) +endif () +if (MATRIX_DOC_PATH) + get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) +endif () +if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) + message( STATUS "Using GTAD at ${ABS_GTAD_PATH}" ) + message( STATUS "Using API files at ${ABS_API_DEF_PATH}" ) + set(API_GENERATION_ENABLED 1) + if (NOT CLANG_FORMAT) + set(CLANG_FORMAT clang-format) + endif() + get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM) + if (ABS_CLANG_FORMAT) + set(API_FORMATTING_ENABLED 1) + message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") + else () + message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") + endif () + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.12.0") # We use globbing with CONFIGURE_DEPENDS to produce two file lists: # one of all API files for clang-format and another of just .cpp @@ -250,6 +238,10 @@ if (MATRIX_DOC_PATH AND GTAD_PATH) endif() endif() endif() +add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" + "build target update-api") +add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" + "formatting of generated API files with clang-format") # Make no mistake: CMake cannot run gtad first and then populate the list of # resulting api_SRCS files. In other words, placing the below statement after @@ -289,15 +281,17 @@ if (${PROJECT_NAME}_ENABLE_E2EE) endif() target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) +configure_file(${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc @ONLY NEWLINE_STYLE UNIX) + +# Configure testing + if (BUILD_TESTING) enable_testing() add_subdirectory(quotest) add_subdirectory(autotests) endif() -configure_file(${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc @ONLY NEWLINE_STYLE UNIX) - -# Installation +# Configure installation install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -338,13 +332,13 @@ if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) -if (${PROJECT_NAME}_INSTALL_TESTS) - install(TARGETS ${TEST_BINARY} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) -endif () - if (UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() +message(STATUS) +feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES + FATAL_ON_MISSING_REQUIRED_PACKAGES) + message(STATUS "<== End of libQuotient configuration") diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index d17e8620..29c53fae 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -6,3 +6,11 @@ set(quotest_SRCS quotest.cpp) add_executable(quotest ${quotest_SRCS}) target_link_libraries(quotest PRIVATE Qt5::Core Qt5::Test ${PROJECT_NAME}) + +option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) +add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS + "the library functional test suite") + +if (${PROJECT_NAME}_INSTALL_TESTS) + install(TARGETS quotest RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif () -- cgit v1.2.3 From e23cddccf0dbafdb5e8d3503d6f6b7491f5b9010 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 14:02:45 +0100 Subject: CI: include github.ref in the quotest origin --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7077ff35..5def16ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV fi - echo "QUOTEST_ORIGIN=${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV + echo "QUOTEST_ORIGIN=${{ github.ref }} @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV echo "DESTDIR=${{ runner.workspace }}" >>$GITHUB_ENV echo "CMAKE_ARGS=-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=${{ runner.workspace }}/usr" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build -- cgit v1.2.3 From 579fc2504be9e5e0291e44da685f10a21a4c64a5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 11:07:45 +0100 Subject: CI: install to ~/.local; invoke quotest from there That way InstallQuotest feature is also tested. Also fix the Valgrind suppression file path. Also: use cmake arguments instead of pushd/popd dance --- .github/workflows/ci.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5def16ee..52580dd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: if: contains(matrix.os, 'ubuntu') run: | sudo apt-get install valgrind - echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=tests/.valgrind.supp" >>$GITHUB_ENV + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=quotest/.valgrind.supp" >>$GITHUB_ENV - name: Setup build environment run: | @@ -62,8 +62,8 @@ jobs: echo "CXX=clang++" >>$GITHUB_ENV fi echo "QUOTEST_ORIGIN=${{ github.ref }} @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - echo "DESTDIR=${{ runner.workspace }}" >>$GITHUB_ENV - echo "CMAKE_ARGS=-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=${{ runner.workspace }}/usr" >>$GITHUB_ENV + echo "CMAKE_ARGS=-DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build - name: Build and install olm @@ -71,10 +71,11 @@ jobs: run: | cd ${{ runner.workspace }} git clone https://gitlab.matrix.org/matrix-org/olm.git - pushd olm - cmake . -B build $CMAKE_ARGS - cmake --build build --target install - popd + cmake -S olm -B olm/build $CMAKE_ARGS + cmake --build olm/build --target install + OLM_SO_PATH=$(dirname $(find ~/.local/lib* -name libolm.so)) + test -n "$OLM_SO_PATH" + echo "DEP_SO_PATH=$OLM_SO_PATH" >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - name: Pull CS API and build GTAD @@ -83,10 +84,8 @@ jobs: cd ${{ runner.workspace }} git clone https://github.com/matrix-org/matrix-doc.git git clone --recursive https://github.com/KitsuneRal/gtad.git - pushd gtad - cmake . $CMAKE_ARGS - cmake --build . - popd + cmake -S gtad -B gtad $CMAKE_ARGS + cmake --build gtad echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc -DGTAD_PATH=${{ runner.workspace }}/gtad/gtad" >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV @@ -98,12 +97,15 @@ jobs: run: cmake --build build --target update-api - name: Build and install libQuotient - run: cmake --build build --target install + run: | + cmake --build build --target install + ls ~/.local/bin/quotest - name: Run tests env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} + LD_LIBRARY_PATH: "${{ env.DEP_SO_PATH }}:${{ env.Qt5_DIR }}/lib" run: | - [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + [[ -z "$TEST_USER" ]] || $VALGRIND ~/.local/bin/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From d4005a2ee5fbd51ae169b8827421f448a4cf8050 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 16:19:50 +0100 Subject: Use Ninja --- .github/workflows/ci.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52580dd9..bebeff8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,24 +45,28 @@ jobs: version: '5.9.9' cached: ${{ steps.cache-qt.outputs.cache-hit }} - - name: Install Valgrind - if: contains(matrix.os, 'ubuntu') + - name: Install Ninja (macOS) + if: ${{ !startsWith(matrix.os, 'ubuntu') }} + uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Install Ninja and Valgrind (Linux) + if: startsWith(matrix.os, 'ubuntu') run: | - sudo apt-get install valgrind + sudo apt-get -qq install ninja-build valgrind echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=quotest/.valgrind.supp" >>$GITHUB_ENV - name: Setup build environment run: | if [ "${{ matrix.compiler }}" == "GCC" ]; then - if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-8'; fi - echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV - echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV + if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-8'; fi + echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV + echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV else - echo "CC=clang" >>$GITHUB_ENV - echo "CXX=clang++" >>$GITHUB_ENV + echo "CC=clang" >>$GITHUB_ENV + echo "CXX=clang++" >>$GITHUB_ENV fi echo "QUOTEST_ORIGIN=${{ github.ref }} @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - echo "CMAKE_ARGS=-DCMAKE_BUILD_TYPE=RelWithDebInfo \ + echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build -- cgit v1.2.3 From 37fde3d877b162e8867416bd1d70ee0808976620 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 17:05:06 +0100 Subject: CI: build libraries statically Shared libraries are a bit of a chore to handle, maybe another time. --- .github/workflows/ci.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bebeff8a..b69d34e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: echo "CXX=clang++" >>$GITHUB_ENV fi echo "QUOTEST_ORIGIN=${{ github.ref }} @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build @@ -77,9 +77,6 @@ jobs: git clone https://gitlab.matrix.org/matrix-org/olm.git cmake -S olm -B olm/build $CMAKE_ARGS cmake --build olm/build --target install - OLM_SO_PATH=$(dirname $(find ~/.local/lib* -name libolm.so)) - test -n "$OLM_SO_PATH" - echo "DEP_SO_PATH=$OLM_SO_PATH" >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - name: Pull CS API and build GTAD @@ -90,7 +87,9 @@ jobs: git clone --recursive https://github.com/KitsuneRal/gtad.git cmake -S gtad -B gtad $CMAKE_ARGS cmake --build gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc -DGTAD_PATH=${{ runner.workspace }}/gtad/gtad" >>$GITHUB_ENV + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc \ + -DGTAD_PATH=${{ runner.workspace }}/gtad/gtad" \ + >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV - name: Configure libQuotient @@ -109,7 +108,7 @@ jobs: env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - LD_LIBRARY_PATH: "${{ env.DEP_SO_PATH }}:${{ env.Qt5_DIR }}/lib" + LD_LIBRARY_PATH: "${{ env.Qt5_DIR }}/lib" run: | [[ -z "$TEST_USER" ]] || $VALGRIND ~/.local/bin/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From 302dbc2df4e55855d1c9a97f679101e9d40a260e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 17:18:12 +0100 Subject: Make quotest origin even more informative --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b69d34e7..946b3073 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,14 @@ jobs: echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV fi - echo "QUOTEST_ORIGIN=${{ github.ref }} @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV + if grep -q 'refs/tags' <<<'${{ github.ref }}'; then + VERSION="$(git describe --tags)" + elif [ '${{ github.ref }}' == 'refs/heads/master' ]; then + VERSION="ci${{ github.run_number }}-$(git rev-parse --short HEAD)" + else + VERSION="$(git describe --all --contains)-ci${{ github.run_number }}-$(git rev-parse --short HEAD)" + fi + echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build -- cgit v1.2.3 From dbbd2c61c912769c8a0063454862fa2168e7afd9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jan 2021 18:28:48 +0100 Subject: Revert to running quotest from the source tree For some reason the installed one doesn't find QtTest dynamic library. --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 946b3073..fd6f489e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,6 @@ jobs: env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - LD_LIBRARY_PATH: "${{ env.Qt5_DIR }}/lib" run: | - [[ -z "$TEST_USER" ]] || $VALGRIND ~/.local/bin/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- 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(-) 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 b850edadde2299b122a5cd17da85e943430e43b7 Mon Sep 17 00:00:00 2001 From: Roland Pallai Date: Thu, 28 Jan 2021 14:58:11 +0100 Subject: Fix rich replies json format (transmit) With this patch it looks like: "m.relates_to": { "m.in_reply_to": { "event_id": "$another:event.com" } } instead of: "m.relates_to": { "event_id": "$another:event.com", "rel_type": "m.in_reply_to" }, So it fits the specification by now. https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies --- lib/events/roommessageevent.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 14824277..3fccb380 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -325,7 +325,10 @@ void TextContent::fillJson(QJsonObject* json) const } if (relatesTo) { json->insert(QStringLiteral("m.relates_to"), - QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } }); + relatesTo->type == RelatesTo::ReplyTypeId() ? + QJsonObject { { relatesTo->type, QJsonObject{ { EventIdKey, relatesTo->eventId } } } } : + QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } } + ); if (relatesTo->type == RelatesTo::ReplacementTypeId()) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { -- cgit v1.2.3 From df6b2d31ec8f2f5890826719e960f450a4968f22 Mon Sep 17 00:00:00 2001 From: Roland Pallai Date: Thu, 28 Jan 2021 15:05:33 +0100 Subject: Fix rich edits (transmit) The new formatted_body was not included into new content on edit due to badly constructed json. --- lib/events/roommessageevent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 14824277..4a733772 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -329,8 +329,8 @@ void TextContent::fillJson(QJsonObject* json) const if (relatesTo->type == RelatesTo::ReplacementTypeId()) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { - json->insert(FormatKey, HtmlContentTypeId); - json->insert(FormattedBodyKey, body); + newContentJson.insert(FormatKey, HtmlContentTypeId); + newContentJson.insert(FormattedBodyKey, body); } json->insert(QStringLiteral("m.new_content"), newContentJson); } -- cgit v1.2.3 From cf661d5e85441f849c0e0faca1c929810d4fd752 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 28 Jan 2021 16:03:28 +0100 Subject: Drop qmake from CI If someone still needs it - fix it; otherwise building with qmake will be dropped in 0.7 --- .appveyor.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 764d56d6..69a58ba0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -35,8 +35,6 @@ before_build: build_script: - cmake %CMAKE_ARGS% %CMAKE_E2EE_ARGS% -H. -Bbuild - cmake --build build -# qmake uses olm just built by CMake - it can't build olm on its own. -- qmake -Wall quotest.pro -- %QMAKE_E2EE_ARGS% && jom #after_build: #- cmake --build build --target install -- cgit v1.2.3 From 9760bc4c22b2abf9732732df22cadb7f7db0641c Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Tue, 9 Feb 2021 21:11:14 +0100 Subject: Update --- quotest/quotest.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index ed1c1d13..2512df0d 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -513,12 +513,7 @@ TEST_IMPL(changeName) TEST_IMPL(showLocalUsername) { auto* const localUser = connection()->user(); - if (localUser->name().contains("@")) { - // it is using the id fallback :( - FAIL_TEST(); - } - FINISH_TEST(true); - return false; + FINISH_TEST(localUser->name().contains("@")); } TEST_IMPL(sendAndRedact) -- cgit v1.2.3 From fb6e3f215774d48f7e0c6000cc7a9ebbc66ceb64 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 15 Feb 2021 21:03:49 +0100 Subject: Update quotest/quotest.cpp Co-authored-by: Alexey Rusakov --- quotest/quotest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 2512df0d..bf29c434 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -513,7 +513,7 @@ TEST_IMPL(changeName) TEST_IMPL(showLocalUsername) { auto* const localUser = connection()->user(); - FINISH_TEST(localUser->name().contains("@")); + FINISH_TEST(!localUser->name().contains("@")); } TEST_IMPL(sendAndRedact) -- cgit v1.2.3 From 81664eaf5a983dbd326e4d24d21c2108ea4d6353 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Feb 2021 20:20:38 +0100 Subject: Uri: support abbreviated types in Matrix URIs As per the latest iteration of MSC2312, room/, user/ and event/ are only supported for parsing and replication but not for emitting from Matrix identifiers. (cherry picked from commit 86f24d1ecf300b82b3a7253b81a2c392669d2c2b) --- lib/uri.cpp | 15 +++++++++++---- quotest/quotest.cpp | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/uri.cpp b/lib/uri.cpp index 4b171e79..291bfcae 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -10,13 +10,19 @@ using namespace Quotient; struct ReplacePair { QByteArray uriString; char sigil; }; -/// Defines bi-directional mapping of path prefixes and sigils +/// \brief Defines bi-directional mapping of path prefixes and sigils +/// +/// When there are two prefixes for the same sigil, the first matching +/// entry for a given sigil is used. static const auto replacePairs = { - ReplacePair { "user/", '@' }, + ReplacePair { "u/", '@' }, + { "user/", '@' }, { "roomid/", '!' }, + { "r/", '#' }, { "room/", '#' }, // The notation for bare event ids is not proposed in MSC2312 but there's // https://github.com/matrix-org/matrix-doc/pull/2644 + { "e/", '$' }, { "event/", '$' } }; @@ -97,7 +103,7 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) case 2: break; case 4: - if (splitPath[2] == "event") + if (splitPath[2] == "event" || splitPath[2] == "e") break; [[fallthrough]]; default: @@ -150,7 +156,8 @@ Uri::Type Uri::type() const { return primaryType_; } Uri::SecondaryType Uri::secondaryType() const { - return pathSegment(*this, 2) == "event" ? EventId : NoSecondaryId; + const auto& type2 = pathSegment(*this, 2); + return type2 == "event" || type2 == "e" ? EventId : NoSecondaryId; } QUrl Uri::toUrl(UriForm form) const diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index bf29c434..a0914c72 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -733,12 +733,15 @@ TEST_IMPL(visitResources) roomId, "matrix:roomid/" + roomId.mid(1), "https://matrix.to/#/%21"/*`!`*/ + roomId.mid(1), roomAlias, "matrix:room/" + roomAlias.mid(1), + "matrix:r/" + roomAlias.mid(1), "https://matrix.to/#/" + roomAlias, }; const QStringList userUris { userId, "matrix:user/" + userId.mid(1), + "matrix:u/" + userId.mid(1), "https://matrix.to/#/" + userId }; const QStringList eventUris { "matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1), + "matrix:r/" + roomAlias.mid(1) + "/e/" + eventId.mid(1), "https://matrix.to/#/" + roomId + '/' + eventId }; // Check that reserved characters are correctly processed. @@ -752,6 +755,7 @@ TEST_IMPL(visitResources) static const QStringList joinByAliasUris { Uri(joinRoomAlias.toUtf8(), {}, joinQuery.mid(1)).toDisplayString(), "matrix:room/" + encodedRoomAliasNoSigil + joinQuery, + "matrix:r/" + encodedRoomAliasNoSigil + joinQuery, "https://matrix.to/#/%23"/*`#`*/ + encodedRoomAliasNoSigil + joinQuery, "https://matrix.to/#/%23" + joinRoomAlias.mid(1) /* unencoded */ + joinQuery }; -- cgit v1.2.3 From ec60e76d585813ea54a22b58d6fabc52f113e4b1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Feb 2021 20:47:36 +0100 Subject: Update a comment that still mentions Riot (cherry picked from commit b25785d294669f2bab7dcd1e3cd1fba61991fe46) --- lib/events/roommessageevent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index a9d9754f..31c0fd9e 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -300,7 +300,7 @@ TextContent::TextContent(const QJsonObject& json) const auto actualJson = isReplacement(relatesTo) ? json.value("m.new_content"_ls).toObject() : json; - // Special-casing the custom matrix.org's (actually, Riot's) way + // Special-casing the custom matrix.org's (actually, Element's) way // of sending HTML messages. if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; -- cgit v1.2.3 From 1f92a5a957f7de38ef7c2bf425b8d6ac7fa1239c Mon Sep 17 00:00:00 2001 From: Arnav Rawat Date: Mon, 22 Feb 2021 13:42:42 -0600 Subject: Adds ability remove the avatar Not possible previously --- lib/user.cpp | 5 +++++ lib/user.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/lib/user.cpp b/lib/user.cpp index 9f0386f4..c4c4fec8 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -154,6 +154,11 @@ bool User::setAvatar(QIODevice* source) return doSetAvatar(source); } +void User::removeAvatar() +{ + connection()->callApi(id(), ""); +} + void User::requestDirectChat() { connection()->requestDirectChat(this); } void User::ignore() { connection()->addToIgnoredUsers(this); } diff --git a/lib/user.h b/lib/user.h index f831865e..d0926189 100644 --- a/lib/user.h +++ b/lib/user.h @@ -117,6 +117,8 @@ public Q_SLOTS: bool setAvatar(const QString& fileName); /// Upload contents of the QIODevice and set that as an avatar bool setAvatar(QIODevice* source); + /// Removes the avatar from the profile + void removeAvatar(); /// Create or find a direct chat with this user /*! The resulting chat is returned asynchronously via * Connection::directChatAvailable() -- cgit v1.2.3 From f5862e8b6fccb85f7080ccddcdea436540588b36 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 26 Feb 2021 15:00:55 +0100 Subject: Add public method to determine if we can change the user password --- lib/connection.cpp | 9 +++++++++ lib/connection.h | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index d773f0d8..91de2e4e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1745,6 +1745,15 @@ QStringList Connection::stableRoomVersions() const return l; } +bool Connection::canChangePassword() const +{ + if (!d->capabilities.changePassword) { + // assume we can + return true; + } + return d->capabilities.changePassword->enabled; +} + inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, const Connection::SupportedRoomVersion& v2) { diff --git a/lib/connection.h b/lib/connection.h index 4f949641..0d22d01f 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -354,6 +354,11 @@ public: * \sa loadingCapabilities */ QVector availableRoomVersions() const; + /// Indicate if the user can change its password from the client. + /// This is often not the case when SSO is enabled. + /// \sa loadingCapabilities + bool canChangePassword() const; + /** * Call this before first sync to load from previously saved file. * -- cgit v1.2.3 From 1335534aa71519ce55bee226ac67c1b0188f4b4b Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 26 Feb 2021 18:04:14 +0100 Subject: Apply suggestion Co-authored-by: Alexey Rusakov --- lib/connection.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 91de2e4e..579c7920 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1747,11 +1747,10 @@ QStringList Connection::stableRoomVersions() const bool Connection::canChangePassword() const { - if (!d->capabilities.changePassword) { - // assume we can - return true; - } - return d->capabilities.changePassword->enabled; + // By default assume we can + return d->capabilities.changePassword + ? d->capabilities.changePassword->enabled + : true; } inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, -- 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/events/simplestateevents.h | 17 +++++++++++++++++ lib/room.cpp | 14 ++++++++++++++ lib/room.h | 2 ++ libQuotient.kdev4 | 4 ++++ 4 files changed, 37 insertions(+) create mode 100644 libQuotient.kdev4 diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index d6261a8f..f22f313d 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -71,4 +71,21 @@ public: QStringList aliases() const { return content().value; } }; REGISTER_EVENT_TYPE(RoomAliasesEvent) + +class RoomPinnedEvent + : public StateEvent> +{ +public: + DEFINE_EVENT_TYPEID("m.room.pinned_messages", RoomPinnedEvent) + + explicit RoomPinnedEvent(const QJsonObject& json) + : StateEvent(typeId(), json, QStringLiteral("pinned")) + { } + explicit RoomPinnedEvent(const QStringList& roomEvents) + : StateEvent(typeId(), matrixTypeId(), {}, + QStringLiteral("pinned"), roomEvents) + { } + QStringList pinnedEvents() const { return content().value; } +}; +REGISTER_EVENT_TYPE(RoomPinnedEvent) } // namespace Quotient 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); diff --git a/lib/room.h b/lib/room.h index a8275ce9..60eadacc 100644 --- a/lib/room.h +++ b/lib/room.h @@ -191,6 +191,7 @@ public: QStringList altAliases() const; QStringList aliases() const; QString displayName() const; + QList pinnedEvents() const; QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; @@ -566,6 +567,7 @@ public Q_SLOTS: SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setPinnedMessages(const QStringList& events); /// Set room aliases on the user's current server void setLocalAliases(const QStringList& aliases); void setTopic(const QString& newTopic); diff --git a/libQuotient.kdev4 b/libQuotient.kdev4 new file mode 100644 index 00000000..2afdb72d --- /dev/null +++ b/libQuotient.kdev4 @@ -0,0 +1,4 @@ +[Project] +CreatedFrom=CMakeLists.txt +Manager=KDevCMakeManager +Name=libQuotient -- cgit v1.2.3 From 847bfdca018ce944360c47c18ffee8a4c3d20f5b Mon Sep 17 00:00:00 2001 From: Arnav Rawat Date: Thu, 11 Mar 2021 16:19:25 -0600 Subject: Whoops --- libQuotient.kdev4 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 libQuotient.kdev4 diff --git a/libQuotient.kdev4 b/libQuotient.kdev4 deleted file mode 100644 index 2afdb72d..00000000 --- a/libQuotient.kdev4 +++ /dev/null @@ -1,4 +0,0 @@ -[Project] -CreatedFrom=CMakeLists.txt -Manager=KDevCMakeManager -Name=libQuotient -- 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(-) 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 8858c40091ed73b730fe62bf565b34cdda1b4e39 Mon Sep 17 00:00:00 2001 From: Vladyslav Hnatiuk Date: Sat, 3 Apr 2021 07:39:41 +0200 Subject: Fix broken links in README. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ed599b3..c993c31e 100644 --- a/README.md +++ b/README.md @@ -85,13 +85,13 @@ a recommended way of linking your application with libQuotient on this platform. Static linkage is the default on Windows/macOS; feel free to experiment with dynamic linking and submit PRs if you get reusable results. -[Quotest](tests), the test application that comes with libQuotient, includes +[Quotest](quotest), the test application that comes with libQuotient, includes most common use cases such as sending messages, uploading files, setting room state etc.; for more extensive usage check out the source code of [Quaternion](https://github.com/quotient-im/Quaternion) (the reference client of Quotient) or [Spectral](https://gitlab.com/b0/spectral). -To ease the first step, `tests/CMakeLists.txt` is a good starting point +To ease the first step, `quotest/CMakeLists.txt` is a good starting point for your own CMake-based project using libQuotient. ## Building the library @@ -155,7 +155,7 @@ You can install the library with CMake: cmake --build . --target install ``` This will also install cmake package config files; once this is done, you -should be able to use [`tests/CMakeLists.txt`](tests/CMakeLists.txt) to compile quotest +should be able to use [`quotest/CMakeLists.txt`](quotest/CMakeLists.txt) to compile quotest with the _installed_ library. Installation of the `quotest` binary along with the rest of the library can be skipped by setting `Quotient_INSTALL_TESTS` to `OFF`. -- cgit v1.2.3 From ae85fd8602cefd1b71db8cb173ed77a873dbe223 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 26 Apr 2021 17:52:49 +0200 Subject: Make it possible to load the user metadata In the normal case there is always a room that is associated with an user. So it is in most of the cases, possible to load the metadata (display name and avatar url) with the help of the room. In some cases, it is not possible. For example, when opening an user matrix link pointing to an user and not to a room. In this case, we need to load the metadata independly of the room, since the user is not linked to a room. --- lib/user.cpp | 19 ++++++++++++------- lib/user.h | 4 ++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/user.cpp b/lib/user.cpp index c4c4fec8..7933c5d9 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -51,13 +51,7 @@ User::User(QString userId, Connection* connection) setObjectName(id()); if (connection->userId() == id()) { // Load profile information for local user. - auto *profileJob = connection->callApi(id()); - connect(profileJob, &BaseJob::result, this, [this, profileJob] { - d->defaultName = profileJob->displayname(); - d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); - emit defaultNameChanged(); - emit defaultAvatarChanged(); - }); + load(); } } @@ -69,6 +63,17 @@ Connection* User::connection() const User::~User() = default; +void User::load() +{ + auto *profileJob = connection()->callApi(id()); + connect(profileJob, &BaseJob::result, this, [this, profileJob] { + d->defaultName = profileJob->displayname(); + d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); + emit defaultNameChanged(); + emit defaultAvatarChanged(); + }); +} + QString User::id() const { return d->id; } bool User::isGuest() const diff --git a/lib/user.h b/lib/user.h index d0926189..e4560843 100644 --- a/lib/user.h +++ b/lib/user.h @@ -130,6 +130,10 @@ public Q_SLOTS: void unmarkIgnore(); /// Check whether the user is in ignore list bool isIgnored() const; + /// Force loading displayName and avartar url. This is required in + /// some cases where the you need to use an user independent of the + /// room. + void load(); Q_SIGNALS: void defaultNameChanged(); -- cgit v1.2.3 From f89ece678c47a54a28c91c2d0ced65ba3e9a6540 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 26 Apr 2021 19:12:08 +0200 Subject: CI: Use GCC 9 where GCC 8 was GitHub images no more have GCC 8. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6f489e..24681460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - name: Setup build environment run: | if [ "${{ matrix.compiler }}" == "GCC" ]; then - if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-8'; fi + if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-9'; fi echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV else -- cgit v1.2.3 From 0c8a819f8a3c31360e3f6044374aa219e93ec21e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 8 May 2021 10:07:54 +0200 Subject: Fix joinedRoom signal not being emitted in some cases An alternative implementation of #463 (and thanks to Carl for spotting the original problem). --- lib/connection.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 579c7920..a384783c 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -807,7 +807,7 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias, // that may add their own slots to finished(). connect(job, &BaseJob::finished, this, [this, job] { if (job->status().good()) - provideRoom(job->roomId()); + provideRoom(job->roomId(), JoinState::Join); }); return job; } @@ -1464,11 +1464,13 @@ Room* Connection::provideRoom(const QString& id, Omittable joinState) room = d->roomMap.value({ id, true }, nullptr); if (room) return room; - // No Invite either, setup a new room object below + // No Invite either, setup a new room object in Join state + joinState = JoinState::Join; } if (!room) { - room = roomFactory()(this, id, joinState.value_or(JoinState::Join)); + Q_ASSERT(joinState.has_value()); + room = roomFactory()(this, id, *joinState); if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; return nullptr; -- cgit v1.2.3 From d3edd940098d2a9dbe511311f35187d5c93cbe9b Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 19 May 2021 23:25:43 +0200 Subject: Add libquotient.kdev4 to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e9b63926..769bdf45 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ build build_dir # IDE project files/directories -.kdev4 +*.kdev4 .directory *.user* .idea -- 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/events/simplestateevents.h | 18 +----------------- lib/room.cpp | 16 ++++++++++++---- lib/room.h | 9 +++++++-- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index f22f313d..c977cb6e 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -55,6 +55,7 @@ namespace EventContent { DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) +DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) class RoomAliasesEvent : public StateEvent> { @@ -71,21 +72,4 @@ public: QStringList aliases() const { return content().value; } }; REGISTER_EVENT_TYPE(RoomAliasesEvent) - -class RoomPinnedEvent - : public StateEvent> -{ -public: - DEFINE_EVENT_TYPEID("m.room.pinned_messages", RoomPinnedEvent) - - explicit RoomPinnedEvent(const QJsonObject& json) - : StateEvent(typeId(), json, QStringLiteral("pinned")) - { } - explicit RoomPinnedEvent(const QStringList& roomEvents) - : StateEvent(typeId(), matrixTypeId(), {}, - QStringLiteral("pinned"), roomEvents) - { } - QStringList pinnedEvents() const { return content().value; } -}; -REGISTER_EVENT_TYPE(RoomPinnedEvent) } // namespace Quotient 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; } diff --git a/lib/room.h b/lib/room.h index 60eadacc..23c0c846 100644 --- a/lib/room.h +++ b/lib/room.h @@ -85,6 +85,8 @@ class Room : public QObject { Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) + Q_PROPERTY(QStringList pinnedEventIds READ pinnedEventIds WRITE setPinnedEvents + NOTIFY pinnedEventsChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) @@ -191,7 +193,9 @@ public: QStringList altAliases() const; QStringList aliases() const; QString displayName() const; - QList pinnedEvents() const; + QStringList pinnedEventIds() const; + // Returns events available locally, use pinnedEventIds() for full list + QVector pinnedEvents() const; QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; @@ -567,7 +571,7 @@ public Q_SLOTS: SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); - void setPinnedMessages(const QStringList& events); + void setPinnedEvents(const QStringList& events); /// Set room aliases on the user's current server void setLocalAliases(const QStringList& aliases); void setTopic(const QString& newTopic); @@ -663,6 +667,7 @@ Q_SIGNALS: void namesChanged(Quotient::Room* room); void displaynameAboutToChange(Quotient::Room* room); void displaynameChanged(Quotient::Room* room, QString oldName); + void pinnedEventsChanged(); void topicChanged(); void avatarChanged(); void userAdded(Quotient::User* user); -- cgit v1.2.3 From 0571ba1fb1948a6cc050230a85201291ababbf04 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 7 Jun 2021 07:55:23 +0200 Subject: Connection::joinRoom() shouldn't enforce room state This is an adjustment to the earlier fix of #471: if a join is immediately followed by a leave (e.g. from another client/bot - you can't do it programmatically from libQuotient) the sync may bring the room already in the Leave state; therefore `joinRoom` should not impose the state but rather ask `provideRoom` to create a `Join` room - just as it's designed when passed an empty `joinState`. --- lib/connection.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a384783c..55067bb7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -800,14 +800,14 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { - auto job = callApi(roomAlias, serverNames); - // Upon completion, ensure a room object in Join state is created - // (or it might already be there due to a sync completing earlier). - // finished() is used here instead of success() to overtake clients - // that may add their own slots to finished(). + auto* const job = callApi(roomAlias, serverNames); + // Upon completion, ensure a room object is created in case it hasn't come + // with a sync yet. If the room object is not there, provideRoom() will + // create it in Join state. finished() is used here instead of success() + // to overtake clients that may add their own slots to finished(). connect(job, &BaseJob::finished, this, [this, job] { if (job->status().good()) - provideRoom(job->roomId(), JoinState::Join); + provideRoom(job->roomId()); }); return job; } -- cgit v1.2.3 From dcbb2cfd0e238f788105d7d249f8aac6ad0823e4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 11 Jun 2021 17:46:58 +0200 Subject: CMakeLists: require at least Qt 5.12; add Qt 6 support --- CMakeLists.txt | 23 +++++++++++++++++++---- autotests/CMakeLists.txt | 2 +- quotest/CMakeLists.txt | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 39b1b03a..9b53a53a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,9 +72,21 @@ message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${ # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) -find_package(Qt5 5.9 REQUIRED Core Network Gui Multimedia Test) -get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) -message(STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}") +option(BUILD_WITH_QT6 "Build Quotient with Qt 6" OFF) + +if (NOT BUILD_WITH_QT6) + # Use Qt5 by default + find_package(Qt5 5.12 QUIET COMPONENTS Core) +endif() +if (NOT Qt5Core_FOUND OR BUILD_WITH_Qt6) + find_package(Qt6 6.2 REQUIRED Core Network Gui Test) # TODO: Multimedia + set(Qt Qt6) +else() + find_package(Qt5 5.12 REQUIRED Core Network Gui Multimedia Test) + set(Qt Qt5) +endif() +get_filename_component($Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) +message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") if (${PROJECT_NAME}_ENABLE_E2EE) if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) @@ -279,7 +291,10 @@ if (${PROJECT_NAME}_ENABLE_E2EE) target_link_libraries(${PROJECT_NAME} QtOlm) set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) +if (Qt STREQUAL Qt5) # Qt 6 hasn't got Multimedia component as yet + target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) +endif() configure_file(${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc @ONLY NEWLINE_STYLE UNIX) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 07f1f046..282ab036 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -7,7 +7,7 @@ include(CMakeParseArguments) function(QUOTIENT_ADD_TEST) cmake_parse_arguments(ARG "" "NAME" "" ${ARGN}) add_executable(${ARG_NAME} ${ARG_NAME}.cpp) - target_link_libraries(${ARG_NAME} Qt5::Core Qt5::Test Quotient) + target_link_libraries(${ARG_NAME} ${Qt}::Core ${Qt}::Test Quotient) add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME}) endfunction() diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index 29c53fae..bf9af796 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -5,7 +5,7 @@ set(quotest_SRCS quotest.cpp) add_executable(quotest ${quotest_SRCS}) -target_link_libraries(quotest PRIVATE Qt5::Core Qt5::Test ${PROJECT_NAME}) +target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${PROJECT_NAME}) option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS -- 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/connection.cpp | 3 +++ lib/room.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 55067bb7..b3006084 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -14,6 +14,9 @@ #include "settings.h" #include "user.h" +// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined +#include "moc_connection.cpp" + #include "csapi/account-data.h" #include "csapi/capabilities.h" #include "csapi/joining.h" 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 ff171e91877048f132955abaa617a26c63632bdf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 20:56:41 +0200 Subject: connection.cpp: erase_if -> remove_if erase_if is now also provided by Qt; doing pretty much the same thing, the Qt implementation only returns the number of removed entries instead of returning a collection of them, however. Worth admitting at this point that the function in connection.cpp has never had the semantics of STL's erase_if() and doesn't quite have the semantics of remove_if() either; but at least it's closer to remove_if(). --- lib/connection.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b3006084..e076957a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -58,7 +58,7 @@ using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() template -HashT erase_if(HashT& hashMap, Pred pred) +HashT remove_if(HashT& hashMap, Pred pred) { HashT removals; for (auto it = hashMap.begin(); it != hashMap.end();) { @@ -668,16 +668,16 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events const auto& usersToDCs = dce.usersToDirectChats(); DirectChatsMap remoteRemovals = - erase_if(directChats, [&usersToDCs, this](auto it) { + remove_if(directChats, [&usersToDCs, this](auto it) { return !( usersToDCs.contains(it.key()->id(), it.value()) || dcLocalAdditions.contains(it.key(), it.value())); }); - erase_if(directChatUsers, [&remoteRemovals](auto it) { + remove_if(directChatUsers, [&remoteRemovals](auto it) { return remoteRemovals.contains(it.value(), it.key()); }); // Remove from dcLocalRemovals what the server already has. - erase_if(dcLocalRemovals, [&remoteRemovals](auto it) { + remove_if(dcLocalRemovals, [&remoteRemovals](auto it) { return remoteRemovals.contains(it.key(), it.value()); }); if (MAIN().isDebugEnabled()) @@ -705,7 +705,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) << "Couldn't get a user object for" << it.key(); } // Remove from dcLocalAdditions what the server already has. - erase_if(dcLocalAdditions, [&remoteAdditions](auto it) { + remove_if(dcLocalAdditions, [&remoteAdditions](auto it) { return remoteAdditions.contains(it.key(), it.value()); }); if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) @@ -1388,7 +1388,7 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user) removals.insert(user, roomId); d->dcLocalRemovals.insert(user, roomId); } else { - removals = erase_if(d->directChats, + removals = remove_if(d->directChats, [&roomId](auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); d->dcLocalRemovals += removals; -- 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 ++++++-- lib/room.h | 2 +- lib/uri.cpp | 10 +++++----- 3 files changed, 12 insertions(+), 8 deletions(-) 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; } diff --git a/lib/room.h b/lib/room.h index a8275ce9..26d0121e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -754,7 +754,7 @@ public: explicit MemberSorter(const Room* r) : room(r) {} bool operator()(User* u1, User* u2) const; - bool operator()(User* u1, const QString& u2name) const; + bool operator()(User* u1, QStringView u2name) const; template typename ContT::size_type lowerBoundIndex(const ContT& c, const ValT& v) const diff --git a/lib/uri.cpp b/lib/uri.cpp index 291bfcae..d8624796 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -70,7 +70,7 @@ static QString pathSegment(const QUrl& url, int which) encodedPath(url).section('/', which, which).toUtf8()); } -static auto decodeFragmentPart(const QStringRef& part) +static auto decodeFragmentPart(QStringView part) { return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); } @@ -98,7 +98,7 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) if (scheme() == "matrix") { // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312 const auto& urlPath = encodedPath(*this); - const auto& splitPath = urlPath.splitRef('/'); + const auto& splitPath = urlPath.split('/'); switch (splitPath.size()) { case 2: break; @@ -128,9 +128,9 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) // so force QUrl to decode everything. auto f = fragment(QUrl::EncodeUnicode); if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch()) - *this = Uri { decodeFragmentPart(m.capturedRef("main")), - decodeFragmentPart(m.capturedRef("sec")), - decodeFragmentPart(m.capturedRef("query")) }; + *this = Uri { decodeFragmentPart(m.capturedView(u"main")), + decodeFragmentPart(m.capturedView(u"sec")), + decodeFragmentPart(m.capturedView(u"query")) }; } } -- cgit v1.2.3 From 620c2fe55b327e555477ed29bd670ddc6b9023d1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 21:01:38 +0200 Subject: Make RequestData compile again This fixes reliance on QIODevice being magically available for std::unique_ptr<> by indirect inclusion. Since Qt 6 this inclusion no more happens, time to #include explicitly. --- lib/jobs/requestdata.cpp | 5 +++++ lib/jobs/requestdata.h | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 047e2920..2c001ccc 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -3,6 +3,7 @@ #include "requestdata.h" +#include #include #include #include @@ -31,4 +32,8 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} +RequestData::RequestData(QIODevice* source) + : _source(std::unique_ptr(source)) +{} + RequestData::~RequestData() = default; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 4958e0f9..21657631 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -24,8 +24,7 @@ public: RequestData(const QByteArray& a = {}); RequestData(const QJsonObject& jo); RequestData(const QJsonArray& ja); - RequestData(QIODevice* source) : _source(std::unique_ptr(source)) - {} + RequestData(QIODevice* source); RequestData(RequestData&&) = default; RequestData& operator=(RequestData&&) = default; ~RequestData(); -- cgit v1.2.3 From 67ea5b45701e6bd5bf244039dc60a134d67a4cab Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 13 Jun 2021 14:08:52 +0200 Subject: Disable the piece depending on Qt Multimedia for Qt 6 Waiting for the Multimedia arrival in Qt 6.2. --- lib/events/roommessageevent.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 31c0fd9e..3f6e475d 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -10,7 +10,9 @@ #include #include #include -#include +#if QT_VERSION_MAJOR < 6 +# include +#endif using namespace Quotient; using namespace EventContent; @@ -149,7 +151,11 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, +#if QT_VERSION_MAJOR < 6 QMediaResource(localUrl).resolution(), +#else + {}, +#endif file.fileName()); if (mimeTypeName.startsWith("audio/")) -- cgit v1.2.3 From b8a78fe7a2370697eea4517ab000130fe1180715 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 13 Jun 2021 14:10:47 +0200 Subject: Exclude code no more needed with Qt6 --- lib/converters.h | 4 +++- lib/settings.cpp | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/converters.h b/lib/converters.h index e07b6ee4..af6c0192 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -221,14 +221,16 @@ template struct JsonConverter> : public JsonArrayConverter> {}; +#if QT_VERSION_MAJOR < 6 // QVector is an alias of QList in Qt6 but not in Qt 5 template struct JsonConverter> : public JsonArrayConverter> {}; +#endif template struct JsonConverter> : public JsonArrayConverter> {}; template <> -struct JsonConverter : public JsonConverter> { +struct JsonConverter : public JsonArrayConverter { static auto dump(const QStringList& sl) { return QJsonArray::fromStringList(sl); diff --git a/lib/settings.cpp b/lib/settings.cpp index 703f4320..1d36db27 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -21,7 +21,9 @@ void Settings::setLegacyNames(const QString& organizationName, Settings::Settings(QObject* parent) : QSettings(parent) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) setIniCodec("UTF-8"); +#endif } void Settings::setValue(const QString& key, const QVariant& value) -- cgit v1.2.3 From beb3a135a336dca654d967b88ea06a7457fbbec1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 22:31:25 +0200 Subject: EncryptionEvent: fix "too perfect forwarding" Now that QMetaType introspects into types, it reveals hidden problems (which is very nice of it). --- lib/events/encryptionevent.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index f9bbab12..65ee4187 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -40,6 +40,7 @@ public: // default value : StateEvent(typeId(), obj) {} + EncryptionEvent(EncryptionEvent&&) = delete; template EncryptionEvent(ArgTs&&... contentArgs) : StateEvent(typeId(), matrixTypeId(), QString(), -- cgit v1.2.3 From faf0122db53e4d010ec1f9f00f40feedd6786e71 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 22:43:33 +0200 Subject: Uri: Fix ambiguity around QChar constructors QChar now accepts more types for construction, and that unraveled concatenation of a Type/SecondaryType character with a QString. To fix it, give the compiler a hint by casting to the enum's underlying type (which also nicely documents that we _actually_ switch from enum to character type). --- lib/uri.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/uri.cpp b/lib/uri.cpp index d8624796..c8843dda 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -186,14 +186,18 @@ QString Uri::primaryId() const if (primaryType_ == Empty || primaryType_ == Invalid) return {}; - const auto& idStem = pathSegment(*this, 1); - return idStem.isEmpty() ? idStem : primaryType_ + idStem; + auto idStem = pathSegment(*this, 1); + if (!idStem.isEmpty()) + idStem.push_front(char(primaryType_)); + return idStem; } QString Uri::secondaryId() const { - const auto& idStem = pathSegment(*this, 3); - return idStem.isEmpty() ? idStem : secondaryType() + idStem; + auto idStem = pathSegment(*this, 3); + if (!idStem.isEmpty()) + idStem.push_front(char(secondaryType())); + return idStem; } static const auto ActionKey = QStringLiteral("action"); -- cgit v1.2.3 From fad6ac5fdee53c349c69afd0ad5e57f340228c6b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 22:45:15 +0200 Subject: Generate a moc file for quotient_common.h Previously Q_NAMESPACE did not require its own moc, somehow blending into others; now it does. --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b53a53a..dfb60ea6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,7 @@ endif() if (NOT Qt5Core_FOUND OR BUILD_WITH_Qt6) find_package(Qt6 6.2 REQUIRED Core Network Gui Test) # TODO: Multimedia set(Qt Qt6) + qt6_wrap_cpp(lib_SRCS lib/quotient_common.h) else() find_package(Qt5 5.12 REQUIRED Core Network Gui Multimedia Test) set(Qt Qt5) @@ -124,7 +125,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) endif () # Set up source files -set(lib_SRCS +list(APPEND lib_SRCS lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp -- cgit v1.2.3 From f4255f83bf5f4bfe03dc7518a89b378d3238c940 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 12 Jun 2021 23:24:19 +0200 Subject: CI: Use Qt 5.12, as required from now --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24681460..00e17f42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v2.11.1 with: - version: '5.9.9' + version: '5.12.10' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Install Ninja (macOS) -- cgit v1.2.3 From ab269f183fdc6f611559d69475543089088b4712 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 13 Jun 2021 09:41:28 +0200 Subject: CI: version Qt cache --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00e17f42..ed251bc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: matrix: os: [ubuntu-18.04, macos-10.15] compiler: [ GCC, Clang ] + qt-version: [ '5.12.10' ] # Not using binary values here, to make the job captions more readable e2ee: [ '', 'E2EE' ] update-api: [ '', 'update-api' ] @@ -37,12 +38,12 @@ jobs: uses: actions/cache@v2 with: path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}-QtCache + key: ${{ runner.os }}-Qt${{ matrix.qt-version }}-cache - name: Install Qt uses: jurplel/install-qt-action@v2.11.1 with: - version: '5.12.10' + version: ${{ matrix.qt-version }} cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Install Ninja (macOS) -- cgit v1.2.3 From 55845602abf09bc5ccee2032a21b95b245d3cedd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 13 Jun 2021 14:23:46 +0200 Subject: BaseJob: FollowRedirectsAttribute -> RedirectPolicyAttribute The latter obsoleted the former since Qt 5.9, actually. --- lib/jobs/basejob.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 48c2996d..c27c6a89 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -288,7 +288,8 @@ void BaseJob::Private::sendRequest() req.setRawHeader("Authorization", QByteArray("Bearer ") + connection->accessToken()); req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground); - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); req.setMaximumRedirectsAllowed(10); req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); req.setAttribute( -- cgit v1.2.3 From 21db98f32bc7e87685c7bd813945cbba75ec0fb7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 13 Jun 2021 17:06:41 +0200 Subject: AppVeyor: disable E2EE building, drop older Qt E2EE will be remade anyway so building it now makes little sense. --- .appveyor.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 69a58ba0..fa031ed8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -9,16 +9,6 @@ environment: - QTDIR: C:\Qt\5.13\msvc2017_64 # Fresh Qt, 64-bit VCVARS: "vcvars64.bat" PLATFORM: - - QTDIR: C:\Qt\5.9\msvc2017_64 # Oldest supported Qt, 64-bit, E2EE - VCVARS: "vcvars64.bat" - QMAKE_E2EE_ARGS: '"DEFINES += Quotient_E2EE_ENABLED USE_INTREE_LIBQOLM" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/olm"' - CMAKE_E2EE_ARGS: '-DQuotient_ENABLE_E2EE=ON -DOlm_DIR=build/olm' - PLATFORM: - - QTDIR: C:\Qt\5.13\msvc2017_64 # Fresh Qt, 64-bit, E2EE - VCVARS: "vcvars64.bat" - QMAKE_E2EE_ARGS: '"DEFINES += Quotient_E2EE_ENABLED USE_INTREE_LIBQOLM" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/olm"' - CMAKE_E2EE_ARGS: '-DQuotient_ENABLE_E2EE=ON -DOlm_DIR=build/olm' - PLATFORM: init: - call "%QTDIR%\bin\qtenv2.bat" @@ -28,12 +18,9 @@ init: before_build: - git submodule update --init --recursive -- git clone https://gitlab.matrix.org/matrix-org/olm.git -- cmake %CMAKE_ARGS% -Holm -Bbuild/olm -- cmake --build build/olm build_script: -- cmake %CMAKE_ARGS% %CMAKE_E2EE_ARGS% -H. -Bbuild +- cmake %CMAKE_ARGS% -H. -Bbuild - cmake --build build #after_build: -- cgit v1.2.3 From 083f62f58bc525d761969133e12a859de9b29648 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 13 Jun 2021 17:11:53 +0200 Subject: CMakeLists: require explicit BUILD_WITH_QT6 for Qt 6 It's not there, it's experimental - people should know what they are doing. --- CMakeLists.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dfb60ea6..d930bbf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,13 +72,9 @@ message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${ # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) -option(BUILD_WITH_QT6 "Build Quotient with Qt 6" OFF) +option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF) -if (NOT BUILD_WITH_QT6) - # Use Qt5 by default - find_package(Qt5 5.12 QUIET COMPONENTS Core) -endif() -if (NOT Qt5Core_FOUND OR BUILD_WITH_Qt6) +if (BUILD_WITH_QT6) find_package(Qt6 6.2 REQUIRED Core Network Gui Test) # TODO: Multimedia set(Qt Qt6) qt6_wrap_cpp(lib_SRCS lib/quotient_common.h) -- cgit v1.2.3 From fefa95b86c31f3b7e46fc28327b23050cbadedb9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 23 Jun 2021 13:41:44 +0200 Subject: Update to the new matrix-doc layout and tooling Among things affecting Quotient, the update involved moving API files from api/ to data/api/, adding extensions to event schema files, and switching from ReStructured Text to Markdown as a lightweight markup language. This commit updates the build system and GTAD configuration to accommodate for these. The build system is also more robust now in choosing whether the update-api target should be provided. Previously the target was provided whenever GTAD_PATH and MATRIX_DOC_PATH were specified, even if they did not point to anything valid. CMake now checks that MATRIX_DOC_PATH is an actual directory and that GTAD_PATH points to an actual file. # Conflicts: # CMakeLists.txt --- CMakeLists.txt | 26 ++++++++++++++++++-------- gtad/gtad.yaml | 4 ++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d930bbf2..555ffa96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -174,16 +174,26 @@ set(FULL_CSAPI_DIR lib/${CSAPI_DIR}) set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) -if (GTAD_PATH) +if (GTAD_PATH AND MATRIX_DOC_PATH) get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) + if (EXISTS ${ABS_GTAD_PATH}) + get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/data/api" REALPATH) + if (NOT IS_DIRECTORY ${ABS_API_DEF_PATH}) + # Check the old place of API files + get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) + endif () + if (IS_DIRECTORY ${ABS_API_DEF_PATH}) + set(API_GENERATION_ENABLED 1) + else () + message( WARNING "${MATRIX_DOC_PATH} doesn't seem to point to a valid matrix-doc repo; disabling API stubs generation") + endif () + else (EXISTS ${ABS_GTAD_PATH}) + message( WARNING "${GTAD_PATH} doesn't exist; disabling API stubs generation") + endif () endif () -if (MATRIX_DOC_PATH) - get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) -endif () -if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) +if (API_GENERATION_ENABLED) message( STATUS "Using GTAD at ${ABS_GTAD_PATH}" ) - message( STATUS "Using API files at ${ABS_API_DEF_PATH}" ) - set(API_GENERATION_ENABLED 1) + message( STATUS "Found API files at ${ABS_API_DEF_PATH}" ) if (NOT CLANG_FORMAT) set(CLANG_FORMAT clang-format) endif() @@ -225,7 +235,7 @@ if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) VERBATIM ) add_custom_target(update-api DEPENDS generate-unformatted-api) - if (ABS_CLANG_FORMAT) + if (EXISTS ${ABS_CLANG_FORMAT}) set(CLANG_FORMAT_ARGS -i -sort-includes ${CLANG_FORMAT_ARGS}) # FIXME: the list of files should be produced after GTAD has run. # For now it's produced at CMake invocation. If file() hasn't found diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index adf5024a..d68cc8a0 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -85,7 +85,7 @@ analyzer: { type: RoomEventPtr, imports: "events/eventloader.h" } - /event.yaml$/: { type: EventPtr, imports: "events/eventloader.h" } - - /m\.room\.member$/: void # Skip resolving; see EventsArray<> below + - /m\.room\.member/: void # Skip resolving; see EventsArray<> below - '/^(\./)?definitions/request_email_validation.yaml$/': title: EmailValidationData - '/^(\./)?definitions/request_msisdn_validation.yaml$/': @@ -109,7 +109,7 @@ analyzer: - /^Notification|Result$/: type: "std::vector<{{1}}>" imports: "events/eventloader.h" - - /m\.room\.member$/: # Only used in an array (see also above) + - /m\.room\.member/: # Only used in an array (see also above) type: "EventsArray" imports: "events/roommemberevent.h" - /state_event.yaml$/: StateEvents -- cgit v1.2.3 From 653932bb30a4c3111dc1f99a641a011f10f21edd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 23 Jun 2021 18:59:51 +0200 Subject: Require ClangFormat 10 or newer, if used --- .clang-format | 31 ++++++++++++++----------------- CONTRIBUTING.md | 6 +++--- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.clang-format b/.clang-format index 4510d46e..72b67488 100644 --- a/.clang-format +++ b/.clang-format @@ -23,16 +23,17 @@ AlignAfterOpenBracket: Align #AlignConsecutiveAssignments: false #AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left -AlignOperands: true +AlignOperands: true # 'Align' since ClangFormat 11 #AlignTrailingComments: false #AllowAllArgumentsOnNextLine: true #AllowAllConstructorInitializersOnNextLine: true #AllowAllParametersOfDeclarationOnNextLine: true -#AllowShortBlocksOnASingleLine: false # 'Empty' since ClangFormat 10 +#AllowShortEnumsOnASingleLine: true +#AllowShortBlocksOnASingleLine: Empty #AllowShortCaseLabelsOnASingleLine: false #AllowShortFunctionsOnASingleLine: All #AllowShortLambdasOnASingleLine: All -#AllowShortIfStatementsOnASingleLine: false # 'Never' since ClangFormat 10 +#AllowShortIfStatementsOnASingleLine: Never #AllowShortLoopsOnASingleLine: false #AlwaysBreakAfterDefinitionReturnType: None # deprecated #AlwaysBreakAfterReturnType: None @@ -43,7 +44,7 @@ AlwaysBreakTemplateDeclarations: Yes BraceWrapping: AfterCaseLabel: false AfterClass: false - AfterControlStatement: false + AfterControlStatement: MultiLine AfterEnum: false AfterFunction: true AfterNamespace: false @@ -61,22 +62,18 @@ BreakBeforeBraces: Custom #BreakBeforeInheritanceComma: false #BreakInheritanceList: BeforeColon #BreakBeforeTernaryOperators: true -#BreakConstructorInitializersBeforeComma: false +#BreakConstructorInitializersBeforeComma: false # deprecated? #BreakConstructorInitializers: BeforeComma #BreakStringLiterals: true ColumnLimit: 80 -CompactNamespaces: false -ConstructorInitializerAllOnOneLineOrOnePerLine: true +#CompactNamespaces: false +#ConstructorInitializerAllOnOneLineOrOnePerLine: false #ConstructorInitializerIndentWidth: 4 #ContinuationIndentWidth: 4 Cpp11BracedListStyle: false #DeriveLineEnding: true #DerivePointerAlignment: false FixNamespaceComments: true -ForEachMacros: - - foreach - - Q_FOREACH - - forever IncludeBlocks: Regroup IncludeCategories: - Regex: '^ Date: Wed, 23 Jun 2021 19:12:26 +0200 Subject: *.mustache: Drop a stray leading end-of-line An SPDX comment in the source code did not collapse entirely. --- gtad/data.h.mustache | 3 +-- gtad/operation.cpp.mustache | 3 +-- gtad/operation.h.mustache | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/gtad/data.h.mustache b/gtad/data.h.mustache index a2193380..1b511262 100644 --- a/gtad/data.h.mustache +++ b/gtad/data.h.mustache @@ -1,8 +1,7 @@ {{! SPDX-FileCopyrightText: 2020 Kitsune Ral SPDX-License-Identifier: LGPL-2.1-or-later -}} -{{>preamble}} +}}{{>preamble}} #pragma once #include "converters.h" diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache index 1d0ae476..5bbc45ec 100644 --- a/gtad/operation.cpp.mustache +++ b/gtad/operation.cpp.mustache @@ -1,8 +1,7 @@ {{! SPDX-FileCopyrightText: 2020 Kitsune Ral SPDX-License-Identifier: LGPL-2.1-or-later -}} -{{>preamble}} +}}{{>preamble}} #include "{{filenameBase}}.h" #include diff --git a/gtad/operation.h.mustache b/gtad/operation.h.mustache index 135eee55..f91dc66c 100644 --- a/gtad/operation.h.mustache +++ b/gtad/operation.h.mustache @@ -1,8 +1,7 @@ {{! SPDX-FileCopyrightText: 2020 Kitsune Ral SPDX-License-Identifier: LGPL-2.1-or-later -}} -{{>preamble}} +}}{{>preamble}} #pragma once #include "jobs/basejob.h" -- cgit v1.2.3 From 0d4315008374d9a4dfb11f934875b1a16670ec74 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 23 Jun 2021 19:12:38 +0200 Subject: Re-generate API files --- lib/application-service/definitions/protocol.h | 9 +- lib/csapi/account-data.h | 4 +- lib/csapi/admin.h | 5 +- lib/csapi/administrative_contact.h | 65 ++++--- lib/csapi/appservice_room_directory.cpp | 4 +- lib/csapi/appservice_room_directory.h | 14 +- lib/csapi/banning.cpp | 4 +- lib/csapi/banning.h | 10 +- lib/csapi/content-repo.h | 74 +++++--- lib/csapi/create_room.h | 210 ++++++++++----------- lib/csapi/cross_signing.cpp | 34 ++++ lib/csapi/cross_signing.h | 72 +++++++ lib/csapi/definitions/cross_signing_key.h | 47 +++++ lib/csapi/definitions/device_keys.h | 8 +- lib/csapi/definitions/event_filter.h | 8 +- lib/csapi/definitions/openid_token.h | 4 +- lib/csapi/definitions/public_rooms_response.h | 8 + lib/csapi/definitions/push_condition.h | 17 +- lib/csapi/definitions/push_rule.h | 4 +- lib/csapi/definitions/request_email_validation.h | 5 +- lib/csapi/definitions/request_msisdn_validation.h | 5 +- lib/csapi/definitions/request_token_response.h | 6 +- lib/csapi/definitions/room_event_filter.h | 25 +-- lib/csapi/definitions/sync_filter.h | 8 +- lib/csapi/definitions/third_party_signed.h | 2 +- lib/csapi/definitions/user_identifier.h | 5 +- lib/csapi/device_management.h | 11 +- lib/csapi/directory.h | 25 +-- lib/csapi/event_context.h | 34 ++-- lib/csapi/filter.h | 12 +- lib/csapi/inviting.cpp | 4 +- lib/csapi/inviting.h | 16 +- lib/csapi/joining.cpp | 8 +- lib/csapi/joining.h | 42 +++-- lib/csapi/keys.h | 73 +++++-- lib/csapi/kicking.h | 9 +- lib/csapi/knocking.cpp | 28 +++ lib/csapi/knocking.h | 58 ++++++ lib/csapi/leaving.cpp | 15 +- lib/csapi/leaving.h | 11 +- lib/csapi/list_joined_rooms.h | 2 +- lib/csapi/list_public_rooms.h | 22 ++- lib/csapi/login.h | 48 +++-- lib/csapi/logout.h | 11 +- lib/csapi/message_pagination.h | 54 +++--- lib/csapi/notifications.h | 15 +- lib/csapi/openid.h | 14 +- lib/csapi/peeking_events.h | 29 ++- lib/csapi/presence.h | 12 +- lib/csapi/profile.h | 16 +- lib/csapi/pusher.h | 38 ++-- lib/csapi/pushrules.h | 42 +++-- lib/csapi/read_markers.h | 2 +- lib/csapi/receipts.h | 4 +- lib/csapi/redaction.h | 14 +- lib/csapi/registration.h | 147 +++++++-------- lib/csapi/report_content.cpp | 6 +- lib/csapi/report_content.h | 3 +- lib/csapi/room_send.h | 10 +- lib/csapi/room_state.h | 33 ++-- lib/csapi/rooms.h | 28 +-- lib/csapi/search.h | 24 +-- lib/csapi/sso_login_redirect.cpp | 24 +++ lib/csapi/sso_login_redirect.h | 36 +++- lib/csapi/tags.h | 4 +- lib/csapi/third_party_membership.h | 15 +- lib/csapi/to_device.cpp | 2 +- lib/csapi/to_device.h | 2 +- lib/csapi/typing.h | 6 +- lib/csapi/users.h | 9 +- lib/csapi/versions.h | 10 +- lib/csapi/voip.h | 5 +- lib/csapi/wellknown.h | 2 +- lib/csapi/whoami.h | 20 +- .../definitions/request_email_validation.h | 6 +- .../definitions/request_msisdn_validation.h | 8 +- 76 files changed, 1119 insertions(+), 607 deletions(-) create mode 100644 lib/csapi/cross_signing.cpp create mode 100644 lib/csapi/cross_signing.h create mode 100644 lib/csapi/definitions/cross_signing_key.h create mode 100644 lib/csapi/knocking.cpp create mode 100644 lib/csapi/knocking.h diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 6aee9c57..213dbf19 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -40,7 +40,7 @@ struct ProtocolInstance { /// provided at the higher level Protocol object. QString icon; - /// Preset values for ``fields`` the client may use to search by. + /// Preset values for `fields` the client may use to search by. QJsonObject fields; /// A unique identifier across all instances. @@ -81,10 +81,9 @@ struct ThirdPartyProtocol { /// A content URI representing an icon for the third party protocol. QString icon; - /// The type definitions for the fields defined in the ``user_fields`` and - /// ``location_fields``. Each entry in those arrays MUST have an entry here. - /// The - /// ``string`` key for this object is field name itself. + /// The type definitions for the fields defined in the `user_fields` and + /// `location_fields`. Each entry in those arrays MUST have an entry here. + /// The `string` key for this object is field name itself. /// /// May be an empty object if no fields are defined. QHash fieldTypes; diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index 9a31596f..0c760e80 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -12,7 +12,7 @@ namespace Quotient { * * Set some account_data for the client. This config is only visible to the user * that set the account_data. The config will be synced to clients in the - * top-level ``account_data``. + * top-level `account_data`. */ class SetAccountDataJob : public BaseJob { public: @@ -65,7 +65,7 @@ public: * * Set some account_data for the client on a given room. This config is only * visible to the user that set the account_data. The config will be synced to - * clients in the per-room ``account_data``. + * clients in the per-room `account_data`. */ class SetAccountDataPerRoomJob : public BaseJob { public: diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index 570bf24a..d4fe639b 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -74,7 +74,10 @@ public: // Result properties /// The Matrix user ID of the user. - QString userId() const { return loadFromJson("user_id"_ls); } + QString userId() const + { + return loadFromJson("user_id"_ls); + } /// Each key is an identifier for one of the user's devices. QHash devices() const diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 1966d533..e436971d 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -93,14 +93,14 @@ struct JsonObjectConverter { * * Adds contact information to the user's account. * - * This endpoint is deprecated in favour of the more specific ``/3pid/add`` - * and ``/3pid/bind`` endpoints. + * This endpoint is deprecated in favour of the more specific `/3pid/add` + * and `/3pid/bind` endpoints. * - * .. Note:: - * Previously this endpoint supported a ``bind`` parameter. This parameter - * has been removed, making this endpoint behave as though it was ``false``. - * This results in this endpoint being an equivalent to ``/3pid/bind`` rather - * than dual-purpose. + * **Note:** + * Previously this endpoint supported a `bind` parameter. This parameter + * has been removed, making this endpoint behave as though it was `false`. + * This results in this endpoint being an equivalent to `/3pid/bind` rather + * than dual-purpose. */ class Post3PIDsJob : public BaseJob { public: @@ -144,7 +144,8 @@ struct JsonObjectConverter { /*! \brief Adds contact information to the user's account. * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * Adds contact information to the user's account. Homeservers should use 3PIDs * added through this endpoint for password resets instead of relying on the @@ -206,7 +207,7 @@ public: * Removes a third party identifier from the user's account. This might not * cause an unbind of the identifier from the identity server. * - * Unlike other endpoints, this endpoint does not take an ``id_access_token`` + * Unlike other endpoints, this endpoint does not take an `id_access_token` * parameter because the homeserver is expected to sign the request to the * identity server instead. */ @@ -222,9 +223,9 @@ public: * * \param idServer * The identity server to unbind from. If not provided, the homeserver - * MUST use the ``id_server`` the identifier was added through. If the - * homeserver does not know the original ``id_server``, it MUST return - * a ``id_server_unbind_result`` of ``no-support``. + * MUST use the `id_server` the identifier was added through. If the + * homeserver does not know the original `id_server`, it MUST return + * a `id_server_unbind_result` of `no-support`. */ explicit Delete3pidFromAccountJob(const QString& medium, const QString& address, @@ -233,8 +234,8 @@ public: // Result properties /// An indicator as to whether or not the homeserver was able to unbind - /// the 3PID from the identity server. ``success`` indicates that the - /// indentity server has unbound the identifier whereas ``no-support`` + /// the 3PID from the identity server. `success` indicates that the + /// indentity server has unbound the identifier whereas `no-support` /// indicates that the identity server refuses to support the request /// or the homeserver was not able to determine an identity server to /// unbind from. @@ -249,7 +250,7 @@ public: * Removes a user's third party identifier from the provided identity server * without removing it from the homeserver. * - * Unlike other endpoints, this endpoint does not take an ``id_access_token`` + * Unlike other endpoints, this endpoint does not take an `id_access_token` * parameter because the homeserver is expected to sign the request to the * identity server instead. */ @@ -265,9 +266,9 @@ public: * * \param idServer * The identity server to unbind from. If not provided, the homeserver - * MUST use the ``id_server`` the identifier was added through. If the - * homeserver does not know the original ``id_server``, it MUST return - * a ``id_server_unbind_result`` of ``no-support``. + * MUST use the `id_server` the identifier was added through. If the + * homeserver does not know the original `id_server`, it MUST return + * a `id_server_unbind_result` of `no-support`. */ explicit Unbind3pidFromAccountJob(const QString& medium, const QString& address, @@ -276,8 +277,8 @@ public: // Result properties /// An indicator as to whether or not the identity server was able to unbind - /// the 3PID. ``success`` indicates that the identity server has unbound the - /// identifier whereas ``no-support`` indicates that the identity server + /// the 3PID. `success` indicates that the identity server has unbound the + /// identifier whereas `no-support` indicates that the identity server /// refuses to support the request or the homeserver was not able to /// determine an identity server to unbind from. QString idServerUnbindResult() const @@ -293,7 +294,9 @@ public: * already associated with an account on this homeserver. This API should * be used to request validation tokens when adding an email address to an * account. This API's parameters and response are identical to that of - * the |/register/email/requestToken|_ endpoint. The homeserver should validate + * the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * endpoint. The homeserver should validate * the email itself, either by sending a validation email itself or by using * a service it has control over. */ @@ -307,9 +310,11 @@ public: * already associated with an account on this homeserver. This API should * be used to request validation tokens when adding an email address to an * account. This API's parameters and response are identical to that of - * the |/register/email/requestToken|_ endpoint. The homeserver should - * validate the email itself, either by sending a validation email itself or - * by using a service it has control over. + * the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * endpoint. The homeserver should validate + * the email itself, either by sending a validation email itself or by + * using a service it has control over. */ explicit RequestTokenTo3PIDEmailJob(const EmailValidationData& body); @@ -331,7 +336,9 @@ public: * already associated with an account on this homeserver. This API should * be used to request validation tokens when adding a phone number to an * account. This API's parameters and response are identical to that of - * the |/register/msisdn/requestToken|_ endpoint. The homeserver should validate + * the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * endpoint. The homeserver should validate * the phone number itself, either by sending a validation message itself or by * using a service it has control over. */ @@ -345,9 +352,11 @@ public: * already associated with an account on this homeserver. This API should * be used to request validation tokens when adding a phone number to an * account. This API's parameters and response are identical to that of - * the |/register/msisdn/requestToken|_ endpoint. The homeserver should - * validate the phone number itself, either by sending a validation message - * itself or by using a service it has control over. + * the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * endpoint. The homeserver should validate + * the phone number itself, either by sending a validation message itself + * or by using a service it has control over. */ explicit RequestTokenTo3PIDMSISDNJob(const MsisdnValidationData& body); diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index e8ec55bf..4d87e4af 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -8,10 +8,10 @@ using namespace Quotient; -UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob( +UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob( const QString& networkId, const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, - QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"), + QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"), QStringLiteral("/_matrix/client/r0") % "/directory/list/appservice/" % networkId % "/" % roomId) { diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index 3fa02a07..56a69592 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -17,11 +17,11 @@ namespace Quotient { * This API is similar to the room directory visibility API used by clients * to update the homeserver's more general room directory. * - * This API requires the use of an application service access token - * (``as_token``) instead of a typical client's access_token. This API cannot be - * invoked by users who are not identified as application services. + * This API requires the use of an application service access token (`as_token`) + * instead of a typical client's access_token. This API cannot be invoked by + * users who are not identified as application services. */ -class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob { +class UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { public: /*! \brief Updates a room's visibility in the application service's room * directory. @@ -38,9 +38,9 @@ public: * Whether the room should be visible (public) in the directory * or not (private). */ - explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, - const QString& roomId, - const QString& visibility); + explicit UpdateAppserviceRoomDirectoryVisibilityJob( + const QString& networkId, const QString& roomId, + const QString& visibility); }; } // namespace Quotient diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 8a8ec664..8e0add1a 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -19,12 +19,14 @@ BanJob::BanJob(const QString& roomId, const QString& userId, setRequestData(std::move(_data)); } -UnbanJob::UnbanJob(const QString& roomId, const QString& userId) +UnbanJob::UnbanJob(const QString& roomId, const QString& userId, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/unban") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); + addParam(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); } diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 37ae91ee..7a9697d3 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -30,7 +30,8 @@ public: * * \param reason * The reason the user has been banned. This will be supplied as the - * ``reason`` on the target's updated `m.room.member`_ event. + * `reason` on the target's updated + * [`m.room.member`](/client-server-api/#mroommember) event. */ explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); @@ -54,8 +55,13 @@ public: * * \param userId * The fully qualified user ID of the user being unbanned. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ - explicit UnbanJob(const QString& roomId, const QString& userId); + explicit UnbanJob(const QString& roomId, const QString& userId, + const QString& reason = {}); }; } // namespace Quotient diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index ed67485c..a41453b2 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -32,7 +32,8 @@ public: // Result properties - /// The `MXC URI`_ to the uploaded content. + /// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the + /// uploaded content. QString contentUri() const { return loadFromJson("content_uri"_ls); @@ -47,10 +48,10 @@ public: /*! \brief Download content from the content repository. * * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) + * The server name from the `mxc://` URI (the authoritory component) * * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) + * The media ID from the `mxc://` URI (the path component) * * \param allowRemote * Indicates to the server that it should not attempt to fetch the media @@ -71,7 +72,10 @@ public: // Result properties /// The content type of the file that was previously uploaded. - QString contentType() const { return reply()->rawHeader("Content-Type"); } + QString contentType() const + { + return reply()->rawHeader("Content-Type"); + } /// The name of the file that was previously uploaded, if set. QString contentDisposition() const @@ -80,7 +84,10 @@ public: } /// The content that was previously uploaded. - QIODevice* data() { return reply(); } + QIODevice* data() + { + return reply(); + } }; /*! \brief Download content from the content repository overriding the file name @@ -95,13 +102,13 @@ public: * name * * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) + * The server name from the `mxc://` URI (the authoritory component) * * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) + * The media ID from the `mxc://` URI (the path component) * * \param fileName - * A filename to give in the ``Content-Disposition`` header. + * A filename to give in the `Content-Disposition` header. * * \param allowRemote * Indicates to the server that it should not attempt to fetch the media @@ -125,9 +132,12 @@ public: // Result properties /// The content type of the file that was previously uploaded. - QString contentType() const { return reply()->rawHeader("Content-Type"); } + QString contentType() const + { + return reply()->rawHeader("Content-Type"); + } - /// The ``fileName`` requested or the name of the file that was previously + /// The `fileName` requested or the name of the file that was previously /// uploaded, if set. QString contentDisposition() const { @@ -135,23 +145,27 @@ public: } /// The content that was previously uploaded. - QIODevice* data() { return reply(); } + QIODevice* data() + { + return reply(); + } }; /*! \brief Download a thumbnail of content from the content repository * * Download a thumbnail of content from the content repository. - * See the `thumbnailing <#thumbnails>`_ section for more information. + * See the [Thumbnails](/client-server-api/#thumbnails) section for more + * information. */ class GetContentThumbnailJob : public BaseJob { public: /*! \brief Download a thumbnail of content from the content repository * * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) + * The server name from the `mxc://` URI (the authoritory component) * * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) + * The media ID from the `mxc://` URI (the path component) * * \param width * The *desired* width of the thumbnail. The actual thumbnail may be @@ -162,8 +176,8 @@ public: * larger than the size specified. * * \param method - * The desired resizing method. See the `thumbnailing <#thumbnails>`_ - * section for more information. + * The desired resizing method. See the + * [Thumbnails](/client-server-api/#thumbnails) section for more information. * * \param allowRemote * Indicates to the server that it should not attempt to fetch @@ -188,10 +202,16 @@ public: // Result properties /// The content type of the thumbnail. - QString contentType() const { return reply()->rawHeader("Content-Type"); } + QString contentType() const + { + return reply()->rawHeader("Content-Type"); + } /// A thumbnail of the requested content. - QIODevice* data() { return reply(); } + QIODevice* data() + { + return reply(); + } }; /*! \brief Get information about a URL for a client @@ -199,11 +219,11 @@ public: * Get information about a URL for the client. Typically this is called when a * client sees a URL in a message and wants to render a preview for the user. * - * .. Note:: - * Clients should consider avoiding this endpoint for URLs posted in encrypted - * rooms. Encrypted rooms often contain more sensitive information the users - * do not want to share with the homeserver, and this can mean that the URLs - * being shared should also not be shared with the homeserver. + * **Note:** + * Clients should consider avoiding this endpoint for URLs posted in encrypted + * rooms. Encrypted rooms often contain more sensitive information the users + * do not want to share with the homeserver, and this can mean that the URLs + * being shared should also not be shared with the homeserver. */ class GetUrlPreviewJob : public BaseJob { public: @@ -235,8 +255,12 @@ public: return loadFromJson>("matrix:image:size"_ls); } - /// An `MXC URI`_ to the image. Omitted if there is no image. - QString ogImage() const { return loadFromJson("og:image"_ls); } + /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image. + /// Omitted if there is no image. + QString ogImage() const + { + return loadFromJson("og:image"_ls); + } }; /*! \brief Get the configuration for the content repository. diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 6a718ff4..8c6af7d4 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -16,47 +16,42 @@ namespace Quotient { * the new room, including checking power levels for each event. It MUST * apply the events implied by the request in the following order: * - * 1. The ``m.room.create`` event itself. Must be the first event in the + * 1. The `m.room.create` event itself. Must be the first event in the * room. * - * 2. An ``m.room.member`` event for the creator to join the room. This is + * 2. An `m.room.member` event for the creator to join the room. This is * needed so the remaining events can be sent. * - * 3. A default ``m.room.power_levels`` event, giving the room creator + * 3. A default `m.room.power_levels` event, giving the room creator * (and not other members) permission to send state events. Overridden - * by the ``power_level_content_override`` parameter. + * by the `power_level_content_override` parameter. * - * 4. Events set by the ``preset``. Currently these are the - * ``m.room.join_rules``, - * ``m.room.history_visibility``, and ``m.room.guest_access`` state events. + * 4. Events set by the `preset`. Currently these are the `m.room.join_rules`, + * `m.room.history_visibility`, and `m.room.guest_access` state events. * - * 5. Events listed in ``initial_state``, in the order that they are + * 5. Events listed in `initial_state`, in the order that they are * listed. * - * 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and - * ``m.room.topic`` state events). + * 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` + * state events). * - * 7. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` - * with - * ``membership: invite`` and ``m.room.third_party_invite``). + * 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with + * `membership: invite` and `m.room.third_party_invite`). * * The available presets do the following with respect to room state: * - * ======================== ============== ====================== - * ================ ========= Preset ``join_rules`` - * ``history_visibility`` ``guest_access`` Other - * ======================== ============== ====================== - * ================ ========= - * ``private_chat`` ``invite`` ``shared`` ``can_join`` - * ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All - * invitees are given the same power level as the room creator. - * ``public_chat`` ``public`` ``shared`` ``forbidden`` - * ======================== ============== ====================== - * ================ ========= + * | Preset | `join_rules` | `history_visibility` | + * `guest_access` | Other | + * |------------------------|--------------|----------------------|----------------|-------| + * | `private_chat` | `invite` | `shared` | `can_join` + * | | | `trusted_private_chat` | `invite` | `shared` | + * `can_join` | All invitees are given the same power level as the room + * creator. | | `public_chat` | `public` | `shared` | + * `forbidden` | | * - * The server will create a ``m.room.create`` event in the room with the + * The server will create a `m.room.create` event in the room with the * requesting user as the creator, alongside other keys provided in the - * ``creation_content``. + * `creation_content`. */ class CreateRoomJob : public BaseJob { public: @@ -68,50 +63,44 @@ public: /// the new room, including checking power levels for each event. It MUST /// apply the events implied by the request in the following order: /// - /// 1. The ``m.room.create`` event itself. Must be the first event in the + /// 1. The `m.room.create` event itself. Must be the first event in the /// room. /// - /// 2. An ``m.room.member`` event for the creator to join the room. This is + /// 2. An `m.room.member` event for the creator to join the room. This is /// needed so the remaining events can be sent. /// - /// 3. A default ``m.room.power_levels`` event, giving the room creator + /// 3. A default `m.room.power_levels` event, giving the room creator /// (and not other members) permission to send state events. Overridden - /// by the ``power_level_content_override`` parameter. + /// by the `power_level_content_override` parameter. /// - /// 4. Events set by the ``preset``. Currently these are the - /// ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state - /// events. + /// 4. Events set by the `preset`. Currently these are the + /// `m.room.join_rules`, + /// `m.room.history_visibility`, and `m.room.guest_access` state events. /// - /// 5. Events listed in ``initial_state``, in the order that they are + /// 5. Events listed in `initial_state`, in the order that they are /// listed. /// - /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and - /// ``m.room.topic`` + /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` /// state events). /// - /// 7. Invite events implied by ``invite`` and ``invite_3pid`` - /// (``m.room.member`` with - /// ``membership: invite`` and ``m.room.third_party_invite``). + /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` + /// with + /// `membership: invite` and `m.room.third_party_invite`). /// /// The available presets do the following with respect to room state: /// - /// ======================== ============== ====================== - /// ================ ========= - /// Preset ``join_rules`` ``history_visibility`` - /// ``guest_access`` Other - /// ======================== ============== ====================== - /// ================ ========= - /// ``private_chat`` ``invite`` ``shared`` ``can_join`` - /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All - /// invitees are given the same power level as the room creator. - /// ``public_chat`` ``public`` ``shared`` ``forbidden`` - /// ======================== ============== ====================== - /// ================ ========= + /// | Preset | `join_rules` | `history_visibility` | + /// `guest_access` | Other | + /// |------------------------|--------------|----------------------|----------------|-------| + /// | `private_chat` | `invite` | `shared` | + /// `can_join` | | | `trusted_private_chat` | `invite` | + /// `shared` | `can_join` | All invitees are given the same + /// power level as the room creator. | | `public_chat` | `public` + /// | `shared` | `forbidden` | | /// - /// The server will create a ``m.room.create`` event in the room with the + /// The server will create a `m.room.create` event in the room with the /// requesting user as the creator, alongside other keys provided in the - /// ``creation_content``. + /// `creation_content`. struct Invite3pid { /// The hostname+port of the identity server which should be used for /// third party identifier lookups. @@ -121,7 +110,7 @@ public: /// r0.5-compatible clients and this specification version. QString idAccessToken; /// The kind of address being passed in the address field, for example - /// ``email``. + /// `email`. QString medium; /// The invitee's third party identifier. QString address; @@ -133,50 +122,44 @@ public: /// the new room, including checking power levels for each event. It MUST /// apply the events implied by the request in the following order: /// - /// 1. The ``m.room.create`` event itself. Must be the first event in the + /// 1. The `m.room.create` event itself. Must be the first event in the /// room. /// - /// 2. An ``m.room.member`` event for the creator to join the room. This is + /// 2. An `m.room.member` event for the creator to join the room. This is /// needed so the remaining events can be sent. /// - /// 3. A default ``m.room.power_levels`` event, giving the room creator + /// 3. A default `m.room.power_levels` event, giving the room creator /// (and not other members) permission to send state events. Overridden - /// by the ``power_level_content_override`` parameter. + /// by the `power_level_content_override` parameter. /// - /// 4. Events set by the ``preset``. Currently these are the - /// ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state - /// events. + /// 4. Events set by the `preset`. Currently these are the + /// `m.room.join_rules`, + /// `m.room.history_visibility`, and `m.room.guest_access` state events. /// - /// 5. Events listed in ``initial_state``, in the order that they are + /// 5. Events listed in `initial_state`, in the order that they are /// listed. /// - /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and - /// ``m.room.topic`` + /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` /// state events). /// - /// 7. Invite events implied by ``invite`` and ``invite_3pid`` - /// (``m.room.member`` with - /// ``membership: invite`` and ``m.room.third_party_invite``). + /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` + /// with + /// `membership: invite` and `m.room.third_party_invite`). /// /// The available presets do the following with respect to room state: /// - /// ======================== ============== ====================== - /// ================ ========= - /// Preset ``join_rules`` ``history_visibility`` - /// ``guest_access`` Other - /// ======================== ============== ====================== - /// ================ ========= - /// ``private_chat`` ``invite`` ``shared`` ``can_join`` - /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All - /// invitees are given the same power level as the room creator. - /// ``public_chat`` ``public`` ``shared`` ``forbidden`` - /// ======================== ============== ====================== - /// ================ ========= + /// | Preset | `join_rules` | `history_visibility` | + /// `guest_access` | Other | + /// |------------------------|--------------|----------------------|----------------|-------| + /// | `private_chat` | `invite` | `shared` | + /// `can_join` | | | `trusted_private_chat` | `invite` | + /// `shared` | `can_join` | All invitees are given the same + /// power level as the room creator. | | `public_chat` | `public` + /// | `shared` | `forbidden` | | /// - /// The server will create a ``m.room.create`` event in the room with the + /// The server will create a `m.room.create` event in the room with the /// requesting user as the creator, alongside other keys provided in the - /// ``creation_content``. + /// `creation_content`. struct StateEvent { /// The type of event to send. QString type; @@ -191,12 +174,12 @@ public: /*! \brief Create a new room * * \param visibility - * A ``public`` visibility indicates that the room will be shown - * in the published room list. A ``private`` visibility will hide + * A `public` visibility indicates that the room will be shown + * in the published room list. A `private` visibility will hide * the room from the published room list. Rooms default to - * ``private`` visibility if this key is not included. NB: This - * should not be confused with ``join_rules`` which also uses the - * word ``public``. + * `private` visibility if this key is not included. NB: This + * should not be confused with `join_rules` which also uses the + * word `public`. * * \param roomAliasName * The desired room alias **local part**. If this is included, a @@ -204,20 +187,20 @@ public: * room. The alias will belong on the *same* homeserver which * created the room. For example, if this was set to "foo" and * sent to the homeserver "example.com" the complete room alias - * would be ``#foo:example.com``. + * would be `#foo:example.com`. * * The complete room alias will become the canonical alias for * the room. * * \param name - * If this is included, an ``m.room.name`` event will be sent + * If this is included, an `m.room.name` event will be sent * into the room to indicate the name of the room. See Room - * Events for more information on ``m.room.name``. + * Events for more information on `m.room.name`. * * \param topic - * If this is included, an ``m.room.topic`` event will be sent + * If this is included, an `m.room.topic` event will be sent * into the room to indicate the topic for the room. See Room - * Events for more information on ``m.room.topic``. + * Events for more information on `m.room.topic`. * * \param invite * A list of user IDs to invite to the room. This will tell the @@ -230,14 +213,14 @@ public: * \param roomVersion * The room version to set for the room. If not provided, the homeserver * is to use its configured default. If provided, the homeserver will return - * a 400 error with the errcode ``M_UNSUPPORTED_ROOM_VERSION`` if it does - * not support the room version. + * a 400 error with the errcode `M_UNSUPPORTED_ROOM_VERSION` if it does not + * support the room version. * * \param creationContent - * Extra keys, such as ``m.federate``, to be added to the content - * of the `m.room.create`_ event. The server will clobber the following - * keys: ``creator``, ``room_version``. Future versions of the - * specification may allow the server to clobber other keys. + * Extra keys, such as `m.federate`, to be added to the content + * of the [`m.room.create`](client-server-api/#mroomcreate) event. The + * server will clobber the following keys: `creator`, `room_version`. Future + * versions of the specification may allow the server to clobber other keys. * * \param initialState * A list of state events to set in the new room. This allows @@ -245,28 +228,30 @@ public: * room. The expected format of the state events are an object * with type, state_key and content keys set. * - * Takes precedence over events set by ``preset``, but gets - * overriden by ``name`` and ``topic`` keys. + * Takes precedence over events set by `preset`, but gets + * overriden by `name` and `topic` keys. * * \param preset * Convenience parameter for setting various default state events * based on a preset. * - * If unspecified, the server should use the ``visibility`` to determine - * which preset to use. A visbility of ``public`` equates to a preset of - * ``public_chat`` and ``private`` visibility equates to a preset of - * ``private_chat``. + * If unspecified, the server should use the `visibility` to determine + * which preset to use. A visbility of `public` equates to a preset of + * `public_chat` and `private` visibility equates to a preset of + * `private_chat`. * * \param isDirect - * This flag makes the server set the ``is_direct`` flag on the - * ``m.room.member`` events sent to the users in ``invite`` and - * ``invite_3pid``. See `Direct Messaging`_ for more information. + * This flag makes the server set the `is_direct` flag on the + * `m.room.member` events sent to the users in `invite` and + * `invite_3pid`. See [Direct + * Messaging](/client-server-api/#direct-messaging) for more information. * * \param powerLevelContentOverride * The power level content to override in the default power level * event. This object is applied on top of the generated - * `m.room.power_levels`_ event content prior to it being sent to the room. - * Defaults to overriding nothing. + * [`m.room.power_levels`](client-server-api/#mroompower_levels) + * event content prior to it being sent to the room. Defaults to + * overriding nothing. */ explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, @@ -283,7 +268,10 @@ public: // Result properties /// The created room's ID. - QString roomId() const { return loadFromJson("room_id"_ls); } + QString roomId() const + { + return loadFromJson("room_id"_ls); + } }; template <> diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp new file mode 100644 index 00000000..9bfc026a --- /dev/null +++ b/lib/csapi/cross_signing.cpp @@ -0,0 +1,34 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "cross_signing.h" + +#include + +using namespace Quotient; + +UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( + const Omittable& masterKey, + const Omittable& selfSigningKey, + const Omittable& userSigningKey) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), + QStringLiteral("/_matrix/client/r0") + % "/keys/device_signing/upload") +{ + QJsonObject _data; + addParam(_data, QStringLiteral("master_key"), masterKey); + addParam(_data, QStringLiteral("self_signing_key"), + selfSigningKey); + addParam(_data, QStringLiteral("user_signing_key"), + userSigningKey); + setRequestData(std::move(_data)); +} + +UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( + const QHash>& signatures) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), + QStringLiteral("/_matrix/client/r0") % "/keys/signatures/upload") +{ + setRequestData(Data(toJson(signatures))); +} diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h new file mode 100644 index 00000000..2ab65e06 --- /dev/null +++ b/lib/csapi/cross_signing.h @@ -0,0 +1,72 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/definitions/cross_signing_key.h" + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Upload cross-signing keys. + * + * Publishes cross-signing keys for the user. + * + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). + */ +class UploadCrossSigningKeysJob : public BaseJob { +public: + /*! \brief Upload cross-signing keys. + * + * \param masterKey + * Optional. The user\'s master key. + * + * \param selfSigningKey + * Optional. The user\'s self-signing key. Must be signed by + * the accompanying master key, or by the user\'s most recently + * uploaded master key if no master key is included in the + * request. + * + * \param userSigningKey + * Optional. The user\'s user-signing key. Must be signed by + * the accompanying master key, or by the user\'s most recently + * uploaded master key if no master key is included in the + * request. + */ + explicit UploadCrossSigningKeysJob( + const Omittable& masterKey = none, + const Omittable& selfSigningKey = none, + const Omittable& userSigningKey = none); +}; + +/*! \brief Upload cross-signing signatures. + * + * Publishes cross-signing signatures for the user. The request body is a + * map from user ID to key ID to signed JSON object. + */ +class UploadCrossSigningSignaturesJob : public BaseJob { +public: + /*! \brief Upload cross-signing signatures. + * + * \param signatures + * The signatures to be published. + */ + explicit UploadCrossSigningSignaturesJob( + const QHash>& signatures = {}); + + // Result properties + + /// A map from user ID to key ID to an error for any signatures + /// that failed. If a signature was invalid, the `errcode` will + /// be set to `M_INVALID_SIGNATURE`. + QHash> failures() const + { + return loadFromJson>>( + "failures"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/cross_signing_key.h b/lib/csapi/definitions/cross_signing_key.h new file mode 100644 index 00000000..0cec8161 --- /dev/null +++ b/lib/csapi/definitions/cross_signing_key.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { +/// Cross signing key +struct CrossSigningKey { + /// The ID of the user the key belongs to. + QString userId; + + /// What the key is used for. + QStringList usage; + + /// The public key. The object must have exactly one property, whose name + /// is in the form `:`, and whose + /// value is the unpadded base64 public key. + QHash keys; + + /// Signatures of the key, calculated using the process described at + /// [Signing JSON](/appendices/#signing-json). Optional for the master key. + /// Other keys must be signed by the user\'s master key. + QJsonObject signatures; +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const CrossSigningKey& pod) + { + addParam<>(jo, QStringLiteral("user_id"), pod.userId); + addParam<>(jo, QStringLiteral("usage"), pod.usage); + addParam<>(jo, QStringLiteral("keys"), pod.keys); + addParam(jo, QStringLiteral("signatures"), pod.signatures); + } + static void fillFrom(const QJsonObject& jo, CrossSigningKey& pod) + { + fromJson(jo.value("user_id"_ls), pod.userId); + fromJson(jo.value("usage"_ls), pod.usage); + fromJson(jo.value("keys"_ls), pod.keys); + fromJson(jo.value("signatures"_ls), pod.signatures); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 3065f218..84ecefae 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -21,15 +21,15 @@ struct DeviceKeys { QStringList algorithms; /// Public identity keys. The names of the properties should be in the - /// format ``:``. The keys themselves should be + /// format `:`. The keys themselves should be /// encoded as specified by the key algorithm. QHash keys; /// Signatures for the device key object. A map from user ID, to a map from - /// ``:`` to the signature. + /// `:` to the signature. /// - /// The signature is calculated using the process described at `Signing - /// JSON`_. + /// The signature is calculated using the process described at [Signing + /// JSON](/appendices/#signing-json). QHash> signatures; }; diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 67497412..c55d4f92 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -14,13 +14,13 @@ struct EventFilter { /// A list of sender IDs to exclude. If this list is absent then no senders /// are excluded. A matching sender will be excluded even if it is listed in - /// the ``'senders'`` filter. + /// the `'senders'` filter. QStringList notSenders; /// A list of event types to exclude. If this list is absent then no event /// types are excluded. A matching type will be excluded even if it is - /// listed in the ``'types'`` filter. A '*' can be used as a wildcard to - /// match any sequence of characters. + /// listed in the `'types'` filter. A '*' can be used as a wildcard to match + /// any sequence of characters. QStringList notTypes; /// A list of senders IDs to include. If this list is absent then all @@ -28,7 +28,7 @@ struct EventFilter { QStringList senders; /// A list of event types to include. If this list is absent then all event - /// types are included. A ``'*'`` can be used as a wildcard to match any + /// types are included. A `'*'` can be used as a wildcard to match any /// sequence of characters. QStringList types; }; diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h index 5e68c376..3c447321 100644 --- a/lib/csapi/definitions/openid_token.h +++ b/lib/csapi/definitions/openid_token.h @@ -11,10 +11,10 @@ namespace Quotient { struct OpenidToken { /// An access token the consumer may use to verify the identity of /// the person who generated the token. This is given to the federation - /// API ``GET /openid/userinfo`` to verify the user's identity. + /// API `GET /openid/userinfo` to verify the user's identity. QString accessToken; - /// The string ``Bearer``. + /// The string `Bearer`. QString tokenType; /// The homeserver domain the consumer should use when attempting to diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 8f30e607..34b447d2 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -37,6 +37,12 @@ struct PublicRoomsChunk { /// The URL for the room's avatar, if one is set. QString avatarUrl; + + /// The room's join rule. When not present, the room is assumed to + /// be `public`. Note that rooms with `invite` join rules are not + /// expected here, but rooms with `knock` rules are given their + /// near-public nature. + QString joinRule; }; template <> @@ -54,6 +60,7 @@ struct JsonObjectConverter { addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); + addParam(jo, QStringLiteral("join_rule"), pod.joinRule); } static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) { @@ -66,6 +73,7 @@ struct JsonObjectConverter { fromJson(jo.value("world_readable"_ls), pod.worldReadable); fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); + fromJson(jo.value("join_rule"_ls), pod.joinRule); } }; diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index a6decf1b..ce66d075 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -9,26 +9,27 @@ namespace Quotient { struct PushCondition { - /// The kind of condition to apply. See `conditions <#conditions>`_ for - /// more information on the allowed kinds and how they work. + /// The kind of condition to apply. See + /// [conditions](/client-server-api/#conditions) for more information on the + /// allowed kinds and how they work. QString kind; - /// Required for ``event_match`` conditions. The dot-separated field of the + /// Required for `event_match` conditions. The dot-separated field of the /// event to match. /// - /// Required for ``sender_notification_permission`` conditions. The field in + /// Required for `sender_notification_permission` conditions. The field in /// the power level event the user needs a minimum power level for. Fields - /// must be specified under the ``notifications`` property in the power - /// level event's ``content``. + /// must be specified under the `notifications` property in the power level + /// event's `content`. QString key; - /// Required for ``event_match`` conditions. The glob-style pattern to + /// Required for `event_match` conditions. The glob-style pattern to /// match against. Patterns with no special glob characters should be /// treated as having asterisks prepended and appended when testing the /// condition. QString pattern; - /// Required for ``room_member_count`` conditions. A decimal integer + /// Required for `room_member_count` conditions. A decimal integer /// optionally prefixed by one of, ==, <, >, >= or <=. A prefix of < matches /// rooms where the member count is strictly less than the given number and /// so forth. If no prefix is present, this parameter defaults to ==. diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 43749bae..135537c1 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -25,10 +25,10 @@ struct PushRule { /// The conditions that must hold true for an event in order for a rule to /// be applied to an event. A rule with no conditions always matches. Only - /// applicable to ``underride`` and ``override`` rules. + /// applicable to `underride` and `override` rules. QVector conditions; - /// The glob-style pattern to match against. Only applicable to ``content`` + /// The glob-style pattern to match against. Only applicable to `content` /// rules. QString pattern; }; diff --git a/lib/csapi/definitions/request_email_validation.h b/lib/csapi/definitions/request_email_validation.h index ab34862e..b1781e27 100644 --- a/lib/csapi/definitions/request_email_validation.h +++ b/lib/csapi/definitions/request_email_validation.h @@ -16,15 +16,14 @@ struct EmailValidationData : RequestEmailValidation { /// 3PID verification. /// /// This parameter is deprecated with a plan to be removed in a future - /// specification version for ``/account/password`` and ``/register`` - /// requests. + /// specification version for `/account/password` and `/register` requests. QString idServer; /// An access token previously registered with the identity server. Servers /// can treat this as optional to distinguish between r0.5-compatible /// clients and this specification version. /// - /// Required if an ``id_server`` is supplied. + /// Required if an `id_server` is supplied. QString idAccessToken; }; diff --git a/lib/csapi/definitions/request_msisdn_validation.h b/lib/csapi/definitions/request_msisdn_validation.h index 8539cd98..4600b48c 100644 --- a/lib/csapi/definitions/request_msisdn_validation.h +++ b/lib/csapi/definitions/request_msisdn_validation.h @@ -16,15 +16,14 @@ struct MsisdnValidationData : RequestMsisdnValidation { /// 3PID verification. /// /// This parameter is deprecated with a plan to be removed in a future - /// specification version for ``/account/password`` and ``/register`` - /// requests. + /// specification version for `/account/password` and `/register` requests. QString idServer; /// An access token previously registered with the identity server. Servers /// can treat this as optional to distinguish between r0.5-compatible /// clients and this specification version. /// - /// Required if an ``id_server`` is supplied. + /// Required if an `id_server` is supplied. QString idAccessToken; }; diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h index 00222839..f9981100 100644 --- a/lib/csapi/definitions/request_token_response.h +++ b/lib/csapi/definitions/request_token_response.h @@ -10,20 +10,20 @@ namespace Quotient { struct RequestTokenResponse { /// The session ID. Session IDs are opaque strings that must consist - /// entirely of the characters ``[0-9a-zA-Z.=_-]``. Their length must not + /// entirely of the characters `[0-9a-zA-Z.=_-]`. Their length must not /// exceed 255 characters and they must not be empty. QString sid; /// An optional field containing a URL where the client must submit the /// validation token to, with identical parameters to the Identity Service - /// API's ``POST /validate/email/submitToken`` endpoint (without the + /// API's `POST /validate/email/submitToken` endpoint (without the /// requirement for an access token). The homeserver must send this token to /// the user (if applicable), who should then be prompted to provide it to /// the client. /// /// If this field is not present, the client can assume that verification /// will happen without the client's involvement provided the homeserver - /// advertises this specification version in the ``/versions`` response + /// advertises this specification version in the `/versions` response /// (ie: r0.5.0). QString submitUrl; }; diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 11e87fde..91caf667 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -11,30 +11,31 @@ namespace Quotient { struct RoomEventFilter : EventFilter { - /// If ``true``, enables lazy-loading of membership events. See - /// `Lazy-loading room members <#lazy-loading-room-members>`_ - /// for more information. Defaults to ``false``. + /// If `true`, enables lazy-loading of membership events. See + /// [Lazy-loading room + /// members](/client-server-api/#lazy-loading-room-members) for more + /// information. Defaults to `false`. Omittable lazyLoadMembers; - /// If ``true``, sends all membership events for all events, even if they - /// have already been sent to the client. Does not apply unless - /// ``lazy_load_members`` is ``true``. See `Lazy-loading room members - /// <#lazy-loading-room-members>`_ for more information. Defaults to - /// ``false``. + /// If `true`, sends all membership events for all events, even if they have + /// already been sent to the client. Does not apply unless + /// `lazy_load_members` is `true`. See [Lazy-loading room + /// members](/client-server-api/#lazy-loading-room-members) for more + /// information. Defaults to `false`. Omittable includeRedundantMembers; /// A list of room IDs to exclude. If this list is absent then no rooms are /// excluded. A matching room will be excluded even if it is listed in the - /// ``'rooms'`` filter. + /// `'rooms'` filter. QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are /// included. QStringList rooms; - /// If ``true``, includes only events with a ``url`` key in their content. - /// If ``false``, excludes those events. If omitted, ``url`` key is not - /// considered for filtering. + /// If `true`, includes only events with a `url` key in their content. If + /// `false`, excludes those events. If omitted, `url` key is not considered + /// for filtering. Omittable containsUrl; }; diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index 9c8f08d5..62e17962 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -14,13 +14,13 @@ namespace Quotient { struct RoomFilter { /// A list of room IDs to exclude. If this list is absent then no rooms are /// excluded. A matching room will be excluded even if it is listed in the - /// ``'rooms'`` filter. This filter is applied before the filters in - /// ``ephemeral``, ``state``, ``timeline`` or ``account_data`` + /// `'rooms'` filter. This filter is applied before the filters in + /// `ephemeral`, `state`, `timeline` or `account_data` QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are - /// included. This filter is applied before the filters in ``ephemeral``, - /// ``state``, ``timeline`` or ``account_data`` + /// included. This filter is applied before the filters in `ephemeral`, + /// `state`, `timeline` or `account_data` QStringList rooms; /// The events that aren't recorded in the room history, e.g. typing and diff --git a/lib/csapi/definitions/third_party_signed.h b/lib/csapi/definitions/third_party_signed.h index 526545d0..7097bda4 100644 --- a/lib/csapi/definitions/third_party_signed.h +++ b/lib/csapi/definitions/third_party_signed.h @@ -7,7 +7,7 @@ #include "converters.h" namespace Quotient { -/// A signature of an ``m.third_party_invite`` token to prove that this user +/// A signature of an `m.third_party_invite` token to prove that this user /// owns a third party identity which has been invited to the room. struct ThirdPartySigned { /// The Matrix ID of the user who issued the invite. diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index dadf6f97..cb585a6a 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -9,8 +9,9 @@ namespace Quotient { /// Identification information for a user struct UserIdentifier { - /// The type of identification. See `Identifier types`_ for supported - /// values and additional property descriptions. + /// The type of identification. See [Identifier + /// types](/client-server-api/#identifier-types) for supported values and + /// additional property descriptions. QString type; /// Identification information for a user diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index 47dc7ec8..e2acea18 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -59,7 +59,10 @@ public: // Result properties /// Device information - Device device() const { return fromJson(jsonData()); } + Device device() const + { + return fromJson(jsonData()); + } }; /*! \brief Update a device @@ -83,7 +86,8 @@ public: /*! \brief Delete a device * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * Deletes the given device, and invalidates any access token associated with it. */ @@ -104,7 +108,8 @@ public: /*! \brief Bulk deletion of devices * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * Deletes the given devices, and invalidates any access token associated with * them. diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 9b109aad..00215cae 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -51,7 +51,10 @@ public: // Result properties /// The room ID for this room alias. - QString roomId() const { return loadFromJson("room_id"_ls); } + QString roomId() const + { + return loadFromJson("room_id"_ls); + } /// A list of servers that are aware of this room alias. QStringList servers() const @@ -68,13 +71,13 @@ public: * instance that room aliases can only be deleted by their creator or a server * administrator. * - * .. Note:: - * Servers may choose to update the ``alt_aliases`` for the - * ``m.room.canonical_alias`` state event in the room when an alias is removed. + * **Note:** + * Servers may choose to update the `alt_aliases` for the + * `m.room.canonical_alias` state event in the room when an alias is removed. * Servers which choose to update the canonical alias event are recommended to, * in addition to their other relevant permission checks, delete the alias and * return a successful response even if the user does not have permission to - * update the ``m.room.canonical_alias`` event. + * update the `m.room.canonical_alias` event. */ class DeleteRoomAliasJob : public BaseJob { public: @@ -99,18 +102,18 @@ public: * given room. * * This endpoint can be called by users who are in the room (external - * users receive an ``M_FORBIDDEN`` error response). If the room's - * ``m.room.history_visibility`` maps to ``world_readable``, any + * users receive an `M_FORBIDDEN` error response). If the room's + * `m.room.history_visibility` maps to `world_readable`, any * user can call this endpoint. * * Servers may choose to implement additional access control checks here, * such as allowing server administrators to view aliases regardless of * membership. * - * .. Note:: - * Clients are recommended not to display this list of aliases prominently - * as they are not curated, unlike those listed in the - * ``m.room.canonical_alias`` state event. + * **Note:** + * Clients are recommended not to display this list of aliases prominently + * as they are not curated, unlike those listed in the `m.room.canonical_alias` + * state event. */ class GetLocalAliasesJob : public BaseJob { public: diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index d82d16ab..6a49769f 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -16,8 +16,8 @@ namespace Quotient { * surrounding an event. * * *Note*: This endpoint supports lazy-loading of room member events. See - * `Lazy-loading room members <#lazy-loading-room-members>`_ for more - * information. + * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) + * for more information. */ class GetEventContextJob : public BaseJob { public: @@ -33,13 +33,13 @@ public: * The maximum number of events to return. Default: 10. * * \param filter - * A JSON ``RoomEventFilter`` to filter the returned events with. The - * filter is only applied to ``events_before``, ``events_after``, and - * ``state``. It is not applied to the ``event`` itself. The filter may - * be applied before or/and after the ``limit`` parameter - whichever the + * A JSON `RoomEventFilter` to filter the returned events with. The + * filter is only applied to `events_before`, `events_after`, and + * `state`. It is not applied to the `event` itself. The filter may + * be applied before or/and after the `limit` parameter - whichever the * homeserver prefers. * - * See `Filtering <#filtering>`_ for more information. + * See [Filtering](/client-server-api/#filtering) for more information. */ explicit GetEventContextJob(const QString& roomId, const QString& eventId, Omittable limit = none, @@ -58,10 +58,16 @@ public: // Result properties /// A token that can be used to paginate backwards with. - QString begin() const { return loadFromJson("start"_ls); } + QString begin() const + { + return loadFromJson("start"_ls); + } /// A token that can be used to paginate forwards with. - QString end() const { return loadFromJson("end"_ls); } + QString end() const + { + return loadFromJson("end"_ls); + } /// A list of room events that happened just before the /// requested event, in reverse-chronological order. @@ -71,7 +77,10 @@ public: } /// Details of the requested event. - RoomEventPtr event() { return takeFromJson("event"_ls); } + RoomEventPtr event() + { + return takeFromJson("event"_ls); + } /// A list of room events that happened just after the /// requested event, in chronological order. @@ -81,7 +90,10 @@ public: } /// The state of the room at the last event returned. - StateEvents state() { return takeFromJson("state"_ls); } + StateEvents state() + { + return takeFromJson("state"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index f07b489c..7e9e14ee 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -32,10 +32,13 @@ public: // Result properties /// The ID of the filter that was created. Cannot start - /// with a ``{`` as this character is used to determine + /// with a `{` as this character is used to determine /// if the filter provided is inline JSON or a previously /// declared filter by homeservers on some APIs. - QString filterId() const { return loadFromJson("filter_id"_ls); } + QString filterId() const + { + return loadFromJson("filter_id"_ls); + } }; /*! \brief Download a filter @@ -64,7 +67,10 @@ public: // Result properties /// The filter definition. - Filter filter() const { return fromJson(jsonData()); } + Filter filter() const + { + return fromJson(jsonData()); + } }; } // namespace Quotient diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 01620f9e..1e2554f4 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -8,12 +8,14 @@ using namespace Quotient; -InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) +InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/invite") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); + addParam(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); } diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index 59a61b89..eb13cc95 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -9,13 +9,12 @@ namespace Quotient { /*! \brief Invite a user to participate in a particular room. - * - * .. _invite-by-user-id-endpoint: * * *Note that there are two forms of this API, which are documented separately. * This version of the API requires that the inviter knows the Matrix * identifier of the invitee. The other is documented in the* - * `third party invites section`_. + * [third party invites + * section](/client-server-api/#post_matrixclientr0roomsroomidinvite-1). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the @@ -25,9 +24,7 @@ namespace Quotient { * join that room. * * If the user was invited to the room, the homeserver will append a - * ``m.room.member`` event to the room. - * - * .. _third party invites section: `invite-by-third-party-id-endpoint`_ + * `m.room.member` event to the room. */ class InviteUserJob : public BaseJob { public: @@ -38,8 +35,13 @@ public: * * \param userId * The fully qualified user ID of the invitee. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ - explicit InviteUserJob(const QString& roomId, const QString& userId); + explicit InviteUserJob(const QString& roomId, const QString& userId, + const QString& reason = {}); }; } // namespace Quotient diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 4761e949..998d0b42 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -9,13 +9,15 @@ using namespace Quotient; JoinRoomByIdJob::JoinRoomByIdJob( - const QString& roomId, const Omittable& thirdPartySigned) + const QString& roomId, const Omittable& thirdPartySigned, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/join") { QJsonObject _data; addParam(_data, QStringLiteral("third_party_signed"), thirdPartySigned); + addParam(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); addExpectedKey("room_id"); } @@ -29,7 +31,8 @@ auto queryToJoinRoom(const QStringList& serverName) JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, - const Omittable& thirdPartySigned) + const Omittable& thirdPartySigned, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), QStringLiteral("/_matrix/client/r0") % "/join/" % roomIdOrAlias, queryToJoinRoom(serverName)) @@ -37,6 +40,7 @@ JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, QJsonObject _data; addParam(_data, QStringLiteral("third_party_signed"), thirdPartySigned); + addParam(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); addExpectedKey("room_id"); } diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index dd936f92..6dcd1351 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -13,7 +13,7 @@ namespace Quotient { /*! \brief Start the requesting user participating in a particular room. * * *Note that this API requires a room ID, not alias.* - * ``/join/{roomIdOrAlias}`` *exists if you have a room alias.* + * `/join/{roomIdOrAlias}` *exists if you have a room alias.* * * This API starts a user participating in a particular room, if that user * is allowed to participate in that room. After this call, the client is @@ -21,7 +21,9 @@ namespace Quotient { * events associated with the room until the user leaves the room. * * After a user has joined a room, the room will appear as an entry in the - * response of the |/initialSync|_ and |/sync|_ APIs. + * response of the + * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ class JoinRoomByIdJob : public BaseJob { public: @@ -32,23 +34,31 @@ public: * * \param thirdPartySigned * If supplied, the homeserver must verify that it matches a pending - * ``m.room.third_party_invite`` event in the room, and perform + * `m.room.third_party_invite` event in the room, and perform * key validity checking if required by the event. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ explicit JoinRoomByIdJob( const QString& roomId, - const Omittable& thirdPartySigned = none); + const Omittable& thirdPartySigned = none, + const QString& reason = {}); // Result properties /// The joined room ID. - QString roomId() const { return loadFromJson("room_id"_ls); } + QString roomId() const + { + return loadFromJson("room_id"_ls); + } }; /*! \brief Start the requesting user participating in a particular room. * * *Note that this API takes either a room ID or alias, unlike* - * ``/room/{roomId}/join``. + * `/room/{roomId}/join`. * * This API starts a user participating in a particular room, if that user * is allowed to participate in that room. After this call, the client is @@ -56,7 +66,9 @@ public: * events associated with the room until the user leaves the room. * * After a user has joined a room, the room will appear as an entry in the - * response of the |/initialSync|_ and |/sync|_ APIs. + * response of the + * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ class JoinRoomJob : public BaseJob { public: @@ -70,18 +82,26 @@ public: * must be participating in the room. * * \param thirdPartySigned - * If a ``third_party_signed`` was supplied, the homeserver must verify - * that it matches a pending ``m.room.third_party_invite`` event in the + * If a `third_party_signed` was supplied, the homeserver must verify + * that it matches a pending `m.room.third_party_invite` event in the * room, and perform key validity checking if required by the event. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ explicit JoinRoomJob( const QString& roomIdOrAlias, const QStringList& serverName = {}, - const Omittable& thirdPartySigned = none); + const Omittable& thirdPartySigned = none, + const QString& reason = {}); // Result properties /// The joined room ID. - QString roomId() const { return loadFromJson("room_id"_ls); } + QString roomId() const + { + return loadFromJson("room_id"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 8f6c8cc9..53ba6495 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,6 +4,7 @@ #pragma once +#include "csapi/definitions/cross_signing_key.h" #include "csapi/definitions/device_keys.h" #include "jobs/basejob.h" @@ -25,8 +26,8 @@ public: * \param oneTimeKeys * One-time public keys for "pre-key" messages. The names of * the properties should be in the format - * ``:``. The format of the key is determined - * by the `key algorithm <#key-algorithms>`_. + * `:`. The format of the key is determined + * by the [key algorithm](/client-server-api/#key-algorithms). * * May be absent if no new one-time keys are required. */ @@ -98,7 +99,7 @@ public: /// /// If the homeserver could be reached, but the user or device /// was unknown, no failure is recorded. Instead, the corresponding - /// user or device is missing from the ``device_keys`` result. + /// user or device is missing from the `device_keys` result. QHash failures() const { return loadFromJson>("failures"_ls); @@ -107,13 +108,45 @@ public: /// Information on the queried devices. A map from user ID, to a /// map from device ID to device information. For each device, /// the information returned will be the same as uploaded via - /// ``/keys/upload``, with the addition of an ``unsigned`` + /// `/keys/upload`, with the addition of an `unsigned` /// property. QHash> deviceKeys() const { return loadFromJson>>( "device_keys"_ls); } + + /// Information on the master cross-signing keys of the queried users. + /// A map from user ID, to master key information. For each key, the + /// information returned will be the same as uploaded via + /// `/keys/device_signing/upload`, along with the signatures + /// uploaded via `/keys/signatures/upload` that the requesting user + /// is allowed to see. + QHash masterKeys() const + { + return loadFromJson>("master_keys"_ls); + } + + /// Information on the self-signing keys of the queried users. A map + /// from user ID, to self-signing key information. For each key, the + /// information returned will be the same as uploaded via + /// `/keys/device_signing/upload`. + QHash selfSigningKeys() const + { + return loadFromJson>( + "self_signing_keys"_ls); + } + + /// Information on the user-signing key of the user making the + /// request, if they queried their own device information. A map + /// from user ID, to user-signing key information. The + /// information returned will be the same as uploaded via + /// `/keys/device_signing/upload`. + QHash userSigningKeys() const + { + return loadFromJson>( + "user_signing_keys"_ls); + } }; template <> @@ -163,18 +196,17 @@ public: /// /// If the homeserver could be reached, but the user or device /// was unknown, no failure is recorded. Instead, the corresponding - /// user or device is missing from the ``one_time_keys`` result. + /// user or device is missing from the `one_time_keys` result. QHash failures() const { return loadFromJson>("failures"_ls); } /// One-time keys for the queried devices. A map from user ID, to a - /// map from devices to a map from ``:`` to the key - /// object. + /// map from devices to a map from `:` to the key object. /// - /// See the `key algorithms <#key-algorithms>`_ section for information - /// on the Key Object format. + /// See the [key algorithms](/client-server-api/#key-algorithms) section for + /// information on the Key Object format. QHash> oneTimeKeys() const { return loadFromJson>>( @@ -190,26 +222,28 @@ public: * The server should include in the results any users who: * * * currently share a room with the calling user (ie, both users have - * membership state ``join``); *and* + * membership state `join`); *and* * * added new device identity keys or removed an existing device with - * identity keys, between ``from`` and ``to``. + * identity keys, between `from` and `to`. */ class GetKeysChangesJob : public BaseJob { public: /*! \brief Query users with recent device key updates. * * \param from - * The desired start point of the list. Should be the ``next_batch`` field - * from a response to an earlier call to |/sync|. Users who have not + * The desired start point of the list. Should be the `next_batch` field + * from a response to an earlier call to + * [`/sync`](/client-server-api/#get_matrixclientr0sync). Users who have not * uploaded new device identity keys since this point, nor deleted * existing devices with identity keys since then, will be excluded * from the results. * * \param to - * The desired end point of the list. Should be the ``next_batch`` - * field from a recent call to |/sync| - typically the most recent - * such call. This may be used by the server as a hint to check its - * caches are up to date. + * The desired end point of the list. Should be the `next_batch` + * field from a recent call to + * [`/sync`](/client-server-api/#get_matrixclientr0sync) - typically the + * most recent such call. This may be used by the server as a hint to check + * its caches are up to date. */ explicit GetKeysChangesJob(const QString& from, const QString& to); @@ -233,7 +267,10 @@ public: /// The Matrix User IDs of all users who may have left all /// the end-to-end encrypted rooms they previously shared /// with the user. - QStringList left() const { return loadFromJson("left"_ls); } + QStringList left() const + { + return loadFromJson("left"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 2645a54f..11018368 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -15,10 +15,10 @@ namespace Quotient { * The caller must have the required power level in order to perform this * operation. * - * Kicking a user adjusts the target member's membership state to be ``leave`` - * with an optional ``reason``. Like with other membership changes, a user can + * Kicking a user adjusts the target member's membership state to be `leave` + * with an optional `reason`. Like with other membership changes, a user can * directly adjust the target member's state by making a request to - * ``/rooms//state/m.room.member/``. + * `/rooms//state/m.room.member/`. */ class KickJob : public BaseJob { public: @@ -32,7 +32,8 @@ public: * * \param reason * The reason the user has been kicked. This will be supplied as the - * ``reason`` on the target's updated `m.room.member`_ event. + * `reason` on the target's updated + * [`m.room.member`](/client-server-api/#mroommember) event. */ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp new file mode 100644 index 00000000..4b561300 --- /dev/null +++ b/lib/csapi/knocking.cpp @@ -0,0 +1,28 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "knocking.h" + +#include + +using namespace Quotient; + +auto queryToKnockRoom(const QStringList& serverName) +{ + BaseJob::Query _q; + addParam(_q, QStringLiteral("server_name"), serverName); + return _q; +} + +KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias, + const QStringList& serverName, const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/knock/" % roomIdOrAlias, + queryToKnockRoom(serverName)) +{ + QJsonObject _data; + addParam(_data, QStringLiteral("reason"), reason); + setRequestData(std::move(_data)); + addExpectedKey("room_id"); +} diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h new file mode 100644 index 00000000..607b55a9 --- /dev/null +++ b/lib/csapi/knocking.h @@ -0,0 +1,58 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Knock on a room, requesting permission to join. + * + * *Note that this API takes either a room ID or alias, unlike other membership + * APIs.* + * + * This API "knocks" on the room to ask for permission to join, if the user + * is allowed to knock on the room. Acceptance of the knock happens out of + * band from this API, meaning that the client will have to watch for updates + * regarding the acceptance/rejection of the knock. + * + * If the room history settings allow, the user will still be able to see + * history of the room while being in the "knock" state. The user will have + * to accept the invitation to join the room (acceptance of knock) to see + * messages reliably. See the `/join` endpoints for more information about + * history visibility to the user. + * + * The knock will appear as an entry in the response of the + * [`/sync`](/client-server-api/#get_matrixclientr0sync) API. + */ +class KnockRoomJob : public BaseJob { +public: + /*! \brief Knock on a room, requesting permission to join. + * + * \param roomIdOrAlias + * The room identifier or alias to knock upon. + * + * \param serverName + * The servers to attempt to knock on the room through. One of the servers + * must be participating in the room. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. + */ + explicit KnockRoomJob(const QString& roomIdOrAlias, + const QStringList& serverName = {}, + const QString& reason = {}); + + // Result properties + + /// The knocked room ID. + QString roomId() const + { + return loadFromJson("room_id"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index 8bd170bf..f4c5f120 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -8,18 +8,15 @@ using namespace Quotient; -QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/leave"); -} - -LeaveRoomJob::LeaveRoomJob(const QString& roomId) +LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/leave") -{} +{ + QJsonObject _data; + addParam(_data, QStringLiteral("reason"), reason); + setRequestData(std::move(_data)); +} QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 1bea7e41..2e402d16 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -28,15 +28,12 @@ public: * * \param roomId * The room identifier to leave. - */ - explicit LeaveRoomJob(const QString& roomId); - - /*! \brief Construct a URL without creating a full-fledged job object * - * This function can be used when a URL for LeaveRoomJob - * is necessary but the job itself isn't. + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + explicit LeaveRoomJob(const QString& roomId, const QString& reason = {}); }; /*! \brief Stop the requesting user remembering about a particular room. diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 1034aa7b..59a24a49 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -26,7 +26,7 @@ public: // Result properties - /// The ID of each room in which the user has ``joined`` membership. + /// The ID of each room in which the user has `joined` membership. QStringList joinedRooms() const { return loadFromJson("joined_rooms"_ls); diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index b0cb79d2..1c73c0af 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -111,12 +111,18 @@ public: /// A pagination token for the response. The absence of this token /// means there are no more results to fetch and the client should /// stop paginating. - QString nextBatch() const { return loadFromJson("next_batch"_ls); } + QString nextBatch() const + { + return loadFromJson("next_batch"_ls); + } /// A pagination token that allows fetching previous results. The /// absence of this token means there are no results before this /// batch, i.e. this is the first batch. - QString prevBatch() const { return loadFromJson("prev_batch"_ls); } + QString prevBatch() const + { + return loadFromJson("prev_batch"_ls); + } /// An estimate on the total number of public rooms, if the /// server has an estimate. @@ -170,7 +176,7 @@ public: * * \param thirdPartyInstanceId * The specific third party network/protocol to request from the - * homeserver. Can only be used if ``include_all_networks`` is false. + * homeserver. Can only be used if `include_all_networks` is false. */ explicit QueryPublicRoomsJob(const QString& server = {}, Omittable limit = none, @@ -190,12 +196,18 @@ public: /// A pagination token for the response. The absence of this token /// means there are no more results to fetch and the client should /// stop paginating. - QString nextBatch() const { return loadFromJson("next_batch"_ls); } + QString nextBatch() const + { + return loadFromJson("next_batch"_ls); + } /// A pagination token that allows fetching previous results. The /// absence of this token means there are no results before this /// batch, i.e. this is the first batch. - QString prevBatch() const { return loadFromJson("prev_batch"_ls); } + QString prevBatch() const + { + return loadFromJson("prev_batch"_ls); + } /// An estimate on the total number of public rooms, if the /// server has an estimate. diff --git a/lib/csapi/login.h b/lib/csapi/login.h index a406fc79..ce783af2 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -14,17 +14,17 @@ namespace Quotient { /*! \brief Get the supported login types to authenticate users * * Gets the homeserver's supported login types to authenticate users. Clients - * should pick one of these and supply it as the ``type`` when logging in. + * should pick one of these and supply it as the `type` when logging in. */ class GetLoginFlowsJob : public BaseJob { public: // Inner data structures /// Gets the homeserver's supported login types to authenticate users. - /// Clients should pick one of these and supply it as the ``type`` when + /// Clients should pick one of these and supply it as the `type` when /// logging in. struct LoginFlow { - /// The login type. This is supplied as the ``type`` when + /// The login type. This is supplied as the `type` when /// logging in. QString type; }; @@ -64,13 +64,14 @@ struct JsonObjectConverter { * Authenticates the user, and issues an access token they can * use to authorize themself in subsequent requests. * - * If the client does not supply a ``device_id``, the server must + * If the client does not supply a `device_id`, the server must * auto-generate one. * - * The returned access token must be associated with the ``device_id`` + * The returned access token must be associated with the `device_id` * supplied by the client or generated by the server. The server may * invalidate any access token previously associated with that device. See - * `Relationship between access tokens and devices`_. + * [Relationship between access tokens and + * devices](/client-server-api/#relationship-between-access-tokens-and-devices). */ class LoginJob : public BaseJob { public: @@ -83,30 +84,33 @@ public: * Authenticates the user, and issues an access token they can * use to authorize themself in subsequent requests. * - * If the client does not supply a ``device_id``, the server must + * If the client does not supply a `device_id`, the server must * auto-generate one. * - * The returned access token must be associated with the ``device_id`` + * The returned access token must be associated with the `device_id` * supplied by the client or generated by the server. The server may * invalidate any access token previously associated with that device. See - * `Relationship between access tokens and devices`_. + * [Relationship between access tokens and + * devices](/client-server-api/#relationship-between-access-tokens-and-devices). * * \param password - * Required when ``type`` is ``m.login.password``. The user's + * Required when `type` is `m.login.password`. The user's * password. * * \param token - * Required when ``type`` is ``m.login.token``. Part of `Token-based`_ - * login. + * Required when `type` is `m.login.token`. Part of Token-based login. * * \param deviceId * ID of the client device. If this does not correspond to a - * known client device, a new device will be created. The server - * will auto-generate a device_id if this is not specified. + * known client device, a new device will be created. The given + * device ID must not be the same as a + * [cross-signing](/client-server-api/#cross-signing) key ID. + * The server will auto-generate a device_id + * if this is not specified. * * \param initialDeviceDisplayName * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. + * if `device_id` corresponds to a known device. */ explicit LoginJob(const QString& type, const Omittable& identifier = none, @@ -117,7 +121,10 @@ public: // Result properties /// The fully-qualified Matrix ID for the account. - QString userId() const { return loadFromJson("user_id"_ls); } + QString userId() const + { + return loadFromJson("user_id"_ls); + } /// An access token for the account. /// This access token can then be used to authorize other requests. @@ -130,8 +137,8 @@ public: /// been registered. /// /// **Deprecated**. Clients should extract the server_name from - /// ``user_id`` (by splitting at the first colon) if they require - /// it. Note also that ``homeserver`` is not spelt this way. + /// `user_id` (by splitting at the first colon) if they require + /// it. Note also that `homeserver` is not spelt this way. QString homeServer() const { return loadFromJson("home_server"_ls); @@ -139,7 +146,10 @@ public: /// ID of the logged-in device. Will be the same as the /// corresponding parameter in the request, if one was specified. - QString deviceId() const { return loadFromJson("device_id"_ls); } + QString deviceId() const + { + return loadFromJson("device_id"_ls); + } /// Optional client configuration provided by the server. If present, /// clients SHOULD use the provided object to reconfigure themselves, diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 78f14e40..2e4c2692 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -12,7 +12,8 @@ namespace Quotient { * * Invalidates an existing access token, so that it can no longer be used for * authorization. The device associated with the access token is also deleted. - * `Device keys <#device-keys>`_ for the device are deleted alongside the device. + * [Device keys](/client-server-api/#device-keys) for the device are deleted + * alongside the device. */ class LogoutJob : public BaseJob { public: @@ -31,10 +32,12 @@ public: * * Invalidates all access tokens for a user, so that they can no longer be used * for authorization. This includes the access token that made this request. All - * devices for the user are also deleted. `Device keys <#device-keys>`_ for the - * device are deleted alongside the device. + * devices for the user are also deleted. [Device + * keys](/client-server-api/#device-keys) for the device are deleted alongside + * the device. * - * This endpoint does not use the `User-Interactive Authentication API`_ because + * This endpoint does not use the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api) because * User-Interactive Authentication is designed to protect against attacks where * the someone gets hold of a single access token then takes over the account. * This endpoint invalidates all access tokens for the user, including the token diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 286d4237..020ef543 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -15,8 +15,8 @@ namespace Quotient { * pagination query parameters to paginate history in the room. * * *Note*: This endpoint supports lazy-loading of room member events. See - * `Lazy-loading room members <#lazy-loading-room-members>`_ for more - * information. + * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) + * for more information. */ class GetRoomEventsJob : public BaseJob { public: @@ -27,8 +27,8 @@ public: * * \param from * The token to start returning events from. This token can be obtained - * from a ``prev_batch`` token returned for each room by the sync API, - * or from a ``start`` or ``end`` token returned by a previous request + * from a `prev_batch` token returned for each room by the sync API, + * or from a `start` or `end` token returned by a previous request * to this endpoint. * * \param dir @@ -36,8 +36,8 @@ public: * * \param to * The token to stop returning events at. This token can be obtained from - * a ``prev_batch`` token returned for each room by the sync endpoint, - * or from a ``start`` or ``end`` token returned by a previous request to + * a `prev_batch` token returned for each room by the sync endpoint, + * or from a `start` or `end` token returned by a previous request to * this endpoint. * * \param limit @@ -64,29 +64,41 @@ public: // Result properties - /// The token the pagination starts from. If ``dir=b`` this will be - /// the token supplied in ``from``. - QString begin() const { return loadFromJson("start"_ls); } + /// The token the pagination starts from. If `dir=b` this will be + /// the token supplied in `from`. + QString begin() const + { + return loadFromJson("start"_ls); + } - /// The token the pagination ends at. If ``dir=b`` this token should + /// The token the pagination ends at. If `dir=b` this token should /// be used again to request even earlier events. - QString end() const { return loadFromJson("end"_ls); } + QString end() const + { + return loadFromJson("end"_ls); + } - /// A list of room events. The order depends on the ``dir`` parameter. - /// For ``dir=b`` events will be in reverse-chronological order, - /// for ``dir=f`` in chronological order, so that events start - /// at the ``from`` point. - RoomEvents chunk() { return takeFromJson("chunk"_ls); } + /// A list of room events. The order depends on the `dir` parameter. + /// For `dir=b` events will be in reverse-chronological order, + /// for `dir=f` in chronological order, so that events start + /// at the `from` point. + RoomEvents chunk() + { + return takeFromJson("chunk"_ls); + } - /// A list of state events relevant to showing the ``chunk``. For example, if - /// ``lazy_load_members`` is enabled in the filter then this may contain - /// the membership events for the senders of events in the ``chunk``. + /// A list of state events relevant to showing the `chunk`. For example, if + /// `lazy_load_members` is enabled in the filter then this may contain + /// the membership events for the senders of events in the `chunk`. /// - /// Unless ``include_redundant_members`` is ``true``, the server + /// Unless `include_redundant_members` is `true`, the server /// may remove membership events which would have already been /// sent to the client in prior calls to this endpoint, assuming /// the membership of those members has not changed. - StateEvents state() { return takeFromJson("state"_ls); } + StateEvents state() + { + return takeFromJson("state"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index ff499c7a..0cc165ce 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -22,7 +22,7 @@ public: /// user has been, or would have been notified about. struct Notification { /// The action(s) to perform when the conditions for this rule are met. - /// See `Push Rules: API`_. + /// See [Push Rules: API](/client-server-api/#push-rules-api). QVector actions; /// The Event object for the event that triggered the notification. EventPtr event; @@ -35,7 +35,7 @@ public: QString roomId; /// The unix timestamp at which the event notification was sent, /// in milliseconds. - int ts; + qint64 ts; }; // Construction/destruction @@ -49,7 +49,7 @@ public: * Limit on the number of events to return in this request. * * \param only - * Allows basic filtering of events returned. Supply ``highlight`` + * Allows basic filtering of events returned. Supply `highlight` * to return only events where the notification had the highlight * tweak set. */ @@ -68,10 +68,13 @@ public: // Result properties - /// The token to supply in the ``from`` param of the next - /// ``/notifications`` request in order to request more + /// The token to supply in the `from` param of the next + /// `/notifications` request in order to request more /// events. If this is absent, there are no more results. - QString nextToken() const { return loadFromJson("next_token"_ls); } + QString nextToken() const + { + return loadFromJson("next_token"_ls); + } /// The list of events that triggered notifications. std::vector notifications() diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index efb5f623..88218c20 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -18,7 +18,7 @@ namespace Quotient { * OpenID. * * The access token generated is only valid for the OpenID API. It cannot - * be used to request another OpenID access token or call ``/sync``, for + * be used to request another OpenID access token or call `/sync`, for * example. */ class RequestOpenIdTokenJob : public BaseJob { @@ -38,11 +38,15 @@ public: // Result properties /// OpenID token information. This response is nearly compatible with the - /// response documented in the `OpenID Connect 1.0 Specification - /// `_ - /// with the only difference being the lack of an ``id_token``. Instead, + /// response documented in the + /// [OpenID Connect 1.0 + /// Specification](http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse) + /// with the only difference being the lack of an `id_token`. Instead, /// the Matrix homeserver's name is provided. - OpenidToken tokenData() const { return fromJson(jsonData()); } + OpenidToken tokenData() const + { + return fromJson(jsonData()); + } }; } // namespace Quotient diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index cecd9f2d..1eee880f 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -13,12 +13,12 @@ namespace Quotient { * * This will listen for new events related to a particular room and return * them to the caller. This will block until an event is received, or until - * the ``timeout`` is reached. + * the `timeout` is reached. * - * This API is the same as the normal ``/events`` endpoint, but can be + * This API is the same as the normal `/events` endpoint, but can be * called by users who have not joined the room. * - * Note that the normal ``/events`` endpoint has been deprecated. This + * Note that the normal `/events` endpoint has been deprecated. This * API will also be deprecated at some point, but its replacement is not * yet known. */ @@ -51,16 +51,25 @@ public: // Result properties - /// A token which correlates to the first value in ``chunk``. This - /// is usually the same token supplied to ``from=``. - QString begin() const { return loadFromJson("start"_ls); } + /// A token which correlates to the first value in `chunk`. This + /// is usually the same token supplied to `from=`. + QString begin() const + { + return loadFromJson("start"_ls); + } - /// A token which correlates to the last value in ``chunk``. This - /// token should be used in the next request to ``/events``. - QString end() const { return loadFromJson("end"_ls); } + /// A token which correlates to the last value in `chunk`. This + /// token should be used in the next request to `/events`. + QString end() const + { + return loadFromJson("end"_ls); + } /// An array of events. - RoomEvents chunk() { return takeFromJson("chunk"_ls); } + RoomEvents chunk() + { + return takeFromJson("chunk"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index a885bf4f..c817ad9f 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -12,7 +12,7 @@ namespace Quotient { * * This API sets the given user's presence state. When setting the status, * the activity time is updated to reflect that activity; the client does - * not need to specify the ``last_active_ago`` field. You cannot set the + * not need to specify the `last_active_ago` field. You cannot set the * presence state of another user. */ class SetPresenceJob : public BaseJob { @@ -55,7 +55,10 @@ public: // Result properties /// This user's presence. - QString presence() const { return loadFromJson("presence"_ls); } + QString presence() const + { + return loadFromJson("presence"_ls); + } /// The length of time in milliseconds since an action was performed /// by this user. @@ -65,7 +68,10 @@ public: } /// The state message for this user if one was set. - QString statusMsg() const { return loadFromJson("status_msg"_ls); } + QString statusMsg() const + { + return loadFromJson("status_msg"_ls); + } /// Whether the user is currently active Omittable currentlyActive() const diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 3858fab2..3cda34f8 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -11,7 +11,7 @@ namespace Quotient { /*! \brief Set the user's display name. * * This API sets the given user's display name. You must have permission to - * set this user's display name, e.g. you need to have their ``access_token``. + * set this user's display name, e.g. you need to have their `access_token`. */ class SetDisplayNameJob : public BaseJob { public: @@ -61,7 +61,7 @@ public: /*! \brief Set the user's avatar URL. * * This API sets the given user's avatar URL. You must have permission to - * set this user's avatar URL, e.g. you need to have their ``access_token``. + * set this user's avatar URL, e.g. you need to have their `access_token`. */ class SetAvatarUrlJob : public BaseJob { public: @@ -101,7 +101,10 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const { return loadFromJson("avatar_url"_ls); } + QString avatarUrl() const + { + return loadFromJson("avatar_url"_ls); + } }; /*! \brief Get this user's profile information. @@ -109,7 +112,7 @@ public: * Get the combined profile information for this user. This API may be used * to fetch the user's own profile information or other users; either * locally or on remote homeservers. This API may return keys which are not - * limited to ``displayname`` or ``avatar_url``. + * limited to `displayname` or `avatar_url`. */ class GetUserProfileJob : public BaseJob { public: @@ -130,7 +133,10 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const { return loadFromJson("avatar_url"_ls); } + QString avatarUrl() const + { + return loadFromJson("avatar_url"_ls); + } /// The user's display name if they have set one, otherwise not present. QString displayname() const diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index ae0050d2..13c9ec25 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -19,7 +19,7 @@ public: /// A dictionary of information for the pusher implementation /// itself. struct PusherData { - /// Required if ``kind`` is ``http``. The URL to use to send + /// Required if `kind` is `http`. The URL to use to send /// notifications to. QString url; /// The format to use when sending notifications to the Push @@ -29,11 +29,11 @@ public: /// Gets all currently active pushers for the authenticated user. struct Pusher { - /// This is a unique identifier for this pusher. See ``/set`` for + /// This is a unique identifier for this pusher. See `/set` for /// more detail. /// Max length, 512 bytes. QString pushkey; - /// The kind of pusher. ``"http"`` is a pusher that + /// The kind of pusher. `"http"` is a pusher that /// sends HTTP pokes. QString kind; /// This is a reverse-DNS style identifier for the application. @@ -104,27 +104,27 @@ struct JsonObjectConverter { /*! \brief Modify a pusher for this user on the homeserver. * - * This endpoint allows the creation, modification and deletion of `pushers`_ - * for this user ID. The behaviour of this endpoint varies depending on the - * values in the JSON body. + * This endpoint allows the creation, modification and deletion of + * [pushers](/client-server-api/#push-notifications) for this user ID. The + * behaviour of this endpoint varies depending on the values in the JSON body. */ class PostPusherJob : public BaseJob { public: // Inner data structures /// A dictionary of information for the pusher implementation - /// itself. If ``kind`` is ``http``, this should contain ``url`` + /// itself. If `kind` is `http`, this should contain `url` /// which is the URL to use to send notifications to. struct PusherData { - /// Required if ``kind`` is ``http``. The URL to use to send + /// Required if `kind` is `http`. The URL to use to send /// notifications to. MUST be an HTTPS URL with a path of - /// ``/_matrix/push/v1/notify``. + /// `/_matrix/push/v1/notify`. QString url; /// The format to send notifications in to Push Gateways if the - /// ``kind`` is ``http``. The details about what fields the + /// `kind` is `http`. The details about what fields the /// homeserver should send to the push gateway are defined in the - /// `Push Gateway Specification`_. Currently the only format - /// available is 'event_id_only'. + /// [Push Gateway Specification](/push-gateway-api/). Currently the only + /// format available is 'event_id_only'. QString format; }; @@ -140,13 +140,13 @@ public: * client has no such concept, use any unique identifier. * Max length, 512 bytes. * - * If the ``kind`` is ``"email"``, this is the email address to + * If the `kind` is `"email"`, this is the email address to * send notifications to. * * \param kind - * The kind of pusher to configure. ``"http"`` makes a pusher that - * sends HTTP pokes. ``"email"`` makes a pusher that emails the - * user with unread notifications. ``null`` deletes the pusher. + * The kind of pusher to configure. `"http"` makes a pusher that + * sends HTTP pokes. `"email"` makes a pusher that emails the + * user with unread notifications. `null` deletes the pusher. * * \param appId * This is a reverse-DNS style identifier for the application. @@ -154,7 +154,7 @@ public: * different platform versions get different app identifiers. * Max length, 64 chars. * - * If the ``kind`` is ``"email"``, this is ``"m.email"``. + * If the `kind` is `"email"`, this is `"m.email"`. * * \param appDisplayName * A string that will allow the user to identify what application @@ -170,7 +170,7 @@ public: * * \param data * A dictionary of information for the pusher implementation - * itself. If ``kind`` is ``http``, this should contain ``url`` + * itself. If `kind` is `http`, this should contain `url` * which is the URL to use to send notifications to. * * \param profileTag @@ -182,7 +182,7 @@ public: * given pushkey and App ID in addition to any others with * different user IDs. Otherwise, the homeserver must remove any * other pushers with the same App ID and pushkey for different - * users. The default is ``false``. + * users. The default is `false`. */ explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index 1c6d5c2d..90d2ce79 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -15,9 +15,9 @@ namespace Quotient { /*! \brief Retrieve all push rulesets. * * Retrieve all push rulesets for this user. Clients can "drill-down" on - * the rulesets by suffixing a ``scope`` to this path e.g. - * ``/pushrules/global/``. This will return a subset of this data under the - * specified key e.g. the ``global`` key. + * the rulesets by suffixing a `scope` to this path e.g. + * `/pushrules/global/`. This will return a subset of this data under the + * specified key e.g. the `global` key. */ class GetPushRulesJob : public BaseJob { public: @@ -49,7 +49,7 @@ public: /*! \brief Retrieve a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -71,8 +71,11 @@ public: // Result properties /// The specific push rule. This will also include keys specific to the - /// rule itself such as the rule's ``actions`` and ``conditions`` if set. - PushRule pushRule() const { return fromJson(jsonData()); } + /// rule itself such as the rule's `actions` and `conditions` if set. + PushRule pushRule() const + { + return fromJson(jsonData()); + } }; /*! \brief Delete a push rule. @@ -84,7 +87,7 @@ public: /*! \brief Delete a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -117,7 +120,7 @@ public: /*! \brief Add or change a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -129,7 +132,7 @@ public: * The action(s) to perform when the conditions for this rule are met. * * \param before - * Use 'before' with a ``rule_id`` as its value to make the new rule the + * Use 'before' with a `rule_id` as its value to make the new rule the * next-most important rule with respect to the given user defined rule. * It is not possible to add a rule relative to a predefined server rule. * @@ -141,10 +144,10 @@ public: * \param conditions * The conditions that must hold true for an event in order for a * rule to be applied to an event. A rule with no conditions - * always matches. Only applicable to ``underride`` and ``override`` rules. + * always matches. Only applicable to `underride` and `override` rules. * * \param pattern - * Only applicable to ``content`` rules. The glob-style pattern to match + * Only applicable to `content` rules. The glob-style pattern to match * against. */ explicit SetPushRuleJob(const QString& scope, const QString& kind, @@ -165,8 +168,8 @@ public: /*! \brief Get whether a push rule is enabled * * \param scope - * Either ``global`` or ``device/`` to specify global - * rules or device rules for the given ``profile_tag``. + * Either `global` or `device/` to specify global + * rules or device rules for the given `profile_tag`. * * \param kind * The kind of rule @@ -188,7 +191,10 @@ public: // Result properties /// Whether the push rule is enabled or not. - bool enabled() const { return loadFromJson("enabled"_ls); } + bool enabled() const + { + return loadFromJson("enabled"_ls); + } }; /*! \brief Enable or disable a push rule. @@ -200,7 +206,7 @@ public: /*! \brief Enable or disable a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -224,8 +230,8 @@ public: /*! \brief The actions for a push rule * * \param scope - * Either ``global`` or ``device/`` to specify global - * rules or device rules for the given ``profile_tag``. + * Either `global` or `device/` to specify global + * rules or device rules for the given `profile_tag`. * * \param kind * The kind of rule @@ -263,7 +269,7 @@ public: /*! \brief Set the actions for a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index 0e122c63..00a2aa0d 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -26,7 +26,7 @@ public: * * \param mRead * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` + * equivalent to calling `/receipt/m.read/$elsewhere:example.org` * and is provided here to save that extra call. */ explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 1fac0acf..7ac093cd 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -27,8 +27,8 @@ public: * The event ID to acknowledge up to. * * \param receipt - * Extra receipt information to attach to ``content`` if any. The - * server will automatically set the ``ts`` field. + * Extra receipt information to attach to `content` if any. The + * server will automatically set the `ts` field. */ explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index 541e433a..f12e6b71 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -15,9 +15,12 @@ namespace Quotient { * * This cannot be undone. * - * Users may redact their own events, and any user with a power level - * greater than or equal to the ``redact`` power level of the room may - * redact events there. + * Any user with a power level greater than or equal to the `m.room.redaction` + * event power level may send redaction events in the room. If the user's power + * level greater is also greater than or equal to the `redact` power level + * of the room, the user may redact events sent by other users. + * + * Server administrators may redact events sent by users on their server. */ class RedactEventJob : public BaseJob { public: @@ -43,7 +46,10 @@ public: // Result properties /// A unique identifier for the event. - QString eventId() const { return loadFromJson("event_id"_ls); } + QString eventId() const + { + return loadFromJson("event_id"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 6864fd47..0ad8b101 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -15,8 +15,9 @@ namespace Quotient { /*! \brief Register for an account on this homeserver. * - * This API endpoint uses the `User-Interactive Authentication API`_, except in - * the cases where a guest account is being registered. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api), except in the + * cases where a guest account is being registered. * * Register for an account on this homeserver. * @@ -31,45 +32,46 @@ namespace Quotient { * If registration is successful, this endpoint will issue an access token * the client can use to authorize itself in subsequent requests. * - * If the client does not supply a ``device_id``, the server must + * If the client does not supply a `device_id`, the server must * auto-generate one. * * The server SHOULD register an account with a User ID based on the - * ``username`` provided, if any. Note that the grammar of Matrix User ID + * `username` provided, if any. Note that the grammar of Matrix User ID * localparts is restricted, so the server MUST either map the provided - * ``username`` onto a ``user_id`` in a logical manner, or reject - * ``username``\s which do not comply to the grammar, with - * ``M_INVALID_USERNAME``. + * `username` onto a `user_id` in a logical manner, or reject + * `username`\s which do not comply to the grammar, with + * `M_INVALID_USERNAME`. * * Matrix clients MUST NOT assume that localpart of the registered - * ``user_id`` matches the provided ``username``. + * `user_id` matches the provided `username`. * - * The returned access token must be associated with the ``device_id`` + * The returned access token must be associated with the `device_id` * supplied by the client or generated by the server. The server may * invalidate any access token previously associated with that device. See - * `Relationship between access tokens and devices`_. + * [Relationship between access tokens and + * devices](/client-server-api/#relationship-between-access-tokens-and-devices). * * When registering a guest account, all parameters in the request body - * with the exception of ``initial_device_display_name`` MUST BE ignored - * by the server. The server MUST pick a ``device_id`` for the account + * with the exception of `initial_device_display_name` MUST BE ignored + * by the server. The server MUST pick a `device_id` for the account * regardless of input. * * Any user ID returned by this API must conform to the grammar given in the - * `Matrix specification <../appendices.html#user-identifiers>`_. + * [Matrix specification](/appendices/#user-identifiers). */ class RegisterJob : public BaseJob { public: /*! \brief Register for an account on this homeserver. * * \param kind - * The kind of account to register. Defaults to ``user``. + * The kind of account to register. Defaults to `user`. * * \param auth * Additional authentication information for the * user-interactive authentication API. Note that this * information is *not* used to define how the registered user * should be authenticated, but is instead used to - * authenticate the ``register`` call itself. + * authenticate the `register` call itself. * * \param username * The basis for the localpart of the desired Matrix ID. If omitted, @@ -85,10 +87,10 @@ public: * * \param initialDeviceDisplayName * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. + * if `device_id` corresponds to a known device. * * \param inhibitLogin - * If true, an ``access_token`` and ``device_id`` should not be + * If true, an `access_token` and `device_id` should not be * returned from this call, therefore preventing an automatic * login. Defaults to false. */ @@ -105,12 +107,15 @@ public: /// The fully-qualified Matrix user ID (MXID) that has been registered. /// /// Any user ID returned by this API must conform to the grammar given in - /// the `Matrix specification <../appendices.html#user-identifiers>`_. - QString userId() const { return loadFromJson("user_id"_ls); } + /// the [Matrix specification](/appendices/#user-identifiers). + QString userId() const + { + return loadFromJson("user_id"_ls); + } /// An access token for the account. /// This access token can then be used to authorize other requests. - /// Required if the ``inhibit_login`` option is false. + /// Required if the `inhibit_login` option is false. QString accessToken() const { return loadFromJson("access_token"_ls); @@ -120,8 +125,8 @@ public: /// been registered. /// /// **Deprecated**. Clients should extract the server_name from - /// ``user_id`` (by splitting at the first colon) if they require - /// it. Note also that ``homeserver`` is not spelt this way. + /// `user_id` (by splitting at the first colon) if they require + /// it. Note also that `homeserver` is not spelt this way. QString homeServer() const { return loadFromJson("home_server"_ls); @@ -129,8 +134,11 @@ public: /// ID of the registered device. Will be the same as the /// corresponding parameter in the request, if one was specified. - /// Required if the ``inhibit_login`` option is false. - QString deviceId() const { return loadFromJson("device_id"_ls); } + /// Required if the `inhibit_login` option is false. + QString deviceId() const + { + return loadFromJson("device_id"_ls); + } }; /*! \brief Begins the validation process for an email to be used during @@ -201,9 +209,9 @@ public: * * Changes the password for an account on this homeserver. * - * This API endpoint uses the `User-Interactive Authentication API`_ to - * ensure the user changing the password is actually the owner of the - * account. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api) to ensure the + * user changing the password is actually the owner of the account. * * An access token should be submitted to this endpoint if the client has * an active session. @@ -224,8 +232,8 @@ public: * Whether the user's other access tokens, and their associated devices, * should be revoked if the request succeeds. * - * When ``false``, the server can still take advantage of `the soft logout - * method <#soft-logout>`_ for the user's remaining devices. + * When `false`, the server can still take advantage of the [soft logout + * method](/client-server-api/#soft-logout) for the user's remaining devices. * * \param auth * Additional authentication information for the user-interactive @@ -242,23 +250,18 @@ public: * The homeserver must check that the given email address **is * associated** with an account on this homeserver. This API should be * used to request validation tokens when authenticating for the - * ``/account/password`` endpoint. + * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * |/register/email/requestToken|_ endpoint, except that - * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * endpoint, except that + * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given email address could be found. The server may instead send an * email to the given address prompting the user to create an account. - * ``M_THREEPID_IN_USE`` may not be returned. + * `M_THREEPID_IN_USE` may not be returned. * * The homeserver should validate the email itself, either by sending a * validation email itself or by using a service it has control over. - * - * - * .. |/register/email/requestToken| replace:: ``/register/email/requestToken`` - * - * .. _/register/email/requestToken: - * #post-matrix-client-r0-register-email-requesttoken */ class RequestTokenToResetPasswordEmailJob : public BaseJob { public: @@ -269,24 +272,18 @@ public: * The homeserver must check that the given email address **is * associated** with an account on this homeserver. This API should be * used to request validation tokens when authenticating for the - * ``/account/password`` endpoint. + * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * |/register/email/requestToken|_ endpoint, except that - * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * endpoint, except that + * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given email address could be found. The server may instead send an * email to the given address prompting the user to create an account. - * ``M_THREEPID_IN_USE`` may not be returned. + * `M_THREEPID_IN_USE` may not be returned. * * The homeserver should validate the email itself, either by sending a * validation email itself or by using a service it has control over. - * - * - * .. |/register/email/requestToken| replace:: - * ``/register/email/requestToken`` - * - * .. _/register/email/requestToken: - * #post-matrix-client-r0-register-email-requesttoken */ explicit RequestTokenToResetPasswordEmailJob(const EmailValidationData& body); @@ -305,22 +302,18 @@ public: * The homeserver must check that the given phone number **is * associated** with an account on this homeserver. This API should be * used to request validation tokens when authenticating for the - * ``/account/password`` endpoint. + * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * |/register/msisdn/requestToken|_ endpoint, except that - * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * endpoint, except that + * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given phone number could be found. The server may instead send the SMS * to the given phone number prompting the user to create an account. - * ``M_THREEPID_IN_USE`` may not be returned. + * `M_THREEPID_IN_USE` may not be returned. * * The homeserver should validate the phone number itself, either by sending a * validation message itself or by using a service it has control over. - * - * .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken`` - * - * .. _/register/msisdn/requestToken: - * #post-matrix-client-r0-register-email-requesttoken */ class RequestTokenToResetPasswordMSISDNJob : public BaseJob { public: @@ -331,23 +324,18 @@ public: * The homeserver must check that the given phone number **is * associated** with an account on this homeserver. This API should be * used to request validation tokens when authenticating for the - * ``/account/password`` endpoint. + * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * |/register/msisdn/requestToken|_ endpoint, except that - * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * endpoint, except that + * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given phone number could be found. The server may instead send the SMS * to the given phone number prompting the user to create an account. - * ``M_THREEPID_IN_USE`` may not be returned. + * `M_THREEPID_IN_USE` may not be returned. * * The homeserver should validate the phone number itself, either by sending * a validation message itself or by using a service it has control over. - * - * .. |/register/msisdn/requestToken| replace:: - * ``/register/msisdn/requestToken`` - * - * .. _/register/msisdn/requestToken: - * #post-matrix-client-r0-register-email-requesttoken */ explicit RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body); @@ -366,7 +354,8 @@ public: * Deactivate the user's account, removing all ability for the user to * login again. * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * An access token should be submitted to this endpoint if the client has * an active session. @@ -374,7 +363,7 @@ public: * The homeserver may change the flows available depending on whether a * valid access token is provided. * - * Unlike other endpoints, this endpoint does not take an ``id_access_token`` + * Unlike other endpoints, this endpoint does not take an `id_access_token` * parameter because the homeserver is expected to sign the request to the * identity server instead. */ @@ -388,11 +377,11 @@ public: * * \param idServer * The identity server to unbind all of the user's 3PIDs from. - * If not provided, the homeserver MUST use the ``id_server`` + * If not provided, the homeserver MUST use the `id_server` * that was originally use to bind each identifier. If the - * homeserver does not know which ``id_server`` that was, - * it must return an ``id_server_unbind_result`` of - * ``no-support``. + * homeserver does not know which `id_server` that was, + * it must return an `id_server_unbind_result` of + * `no-support`. */ explicit DeactivateAccountJob(const Omittable& auth = none, const QString& idServer = {}); @@ -400,12 +389,12 @@ public: // Result properties /// An indicator as to whether or not the homeserver was able to unbind - /// the user's 3PIDs from the identity server(s). ``success`` indicates + /// the user's 3PIDs from the identity server(s). `success` indicates /// that all identifiers have been unbound from the identity server while - /// ``no-support`` indicates that one or more identifiers failed to unbind + /// `no-support` indicates that one or more identifiers failed to unbind /// due to the identity server refusing the request or the homeserver /// being unable to determine an identity server to unbind from. This - /// must be ``success`` if the homeserver has no identifiers to unbind + /// must be `success` if the homeserver has no identifiers to unbind /// for the user. QString idServerUnbindResult() const { @@ -447,7 +436,7 @@ public: // Result properties /// A flag to indicate that the username is available. This should always - /// be ``true`` when the server replies with 200 OK. + /// be `true` when the server replies with 200 OK. Omittable available() const { return loadFromJson>("available"_ls); diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index 0a41625f..ea906380 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -9,13 +9,13 @@ using namespace Quotient; ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, - int score, const QString& reason) + Omittable score, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/report/" % eventId) { QJsonObject _data; - addParam<>(_data, QStringLiteral("score"), score); - addParam<>(_data, QStringLiteral("reason"), reason); + addParam(_data, QStringLiteral("score"), score); + addParam(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); } diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index 375e1829..e401c2e1 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -31,7 +31,8 @@ public: * The reason the content is being reported. May be blank. */ explicit ReportContentJob(const QString& roomId, const QString& eventId, - int score, const QString& reason); + Omittable score = none, + const QString& reason = {}); }; } // namespace Quotient diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 39460aca..a9e7ca13 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -16,7 +16,7 @@ namespace Quotient { * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the m. event specification. + * [Room Events](/client-server-api/#room-events) for the m. event specification. */ class SendMessageJob : public BaseJob { public: @@ -40,7 +40,8 @@ public: * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the m. event specification. + * [Room Events](/client-server-api/#room-events) for the m. event + * specification. */ explicit SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body = {}); @@ -48,7 +49,10 @@ public: // Result properties /// A unique identifier for the event. - QString eventId() const { return loadFromJson("event_id"_ls); } + QString eventId() const + { + return loadFromJson("event_id"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index 447605ff..99eb4fc9 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -9,23 +9,21 @@ namespace Quotient { /*! \brief Send a state event to the given room. - * - * .. For backwards compatibility with older links... - * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`: * * State events can be sent using this endpoint. These events will be - * overwritten if ````, ```` and ```` all + * overwritten if ``, `` and `` all * match. * * Requests to this endpoint **cannot use transaction IDs** - * like other ``PUT`` paths because they cannot be differentiated from the - * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. + * like other `PUT` paths because they cannot be differentiated from the + * `state_key`. Furthermore, `POST` is unsupported on state paths. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the ``m.`` event specification. + * [Room Events](/client-server-api/#room-events) for the `m.` event + * specification. * - * If the event type being sent is ``m.room.canonical_alias`` servers + * If the event type being sent is `m.room.canonical_alias` servers * SHOULD ensure that any new aliases being listed in the event are valid * per their grammar/syntax and that they point to the room ID where the * state event is to be sent. Servers do not validate aliases which are @@ -46,22 +44,20 @@ public: * an empty string, the trailing slash on this endpoint is optional. * * \param body - * .. For backwards compatibility with older links... - * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`: - * * State events can be sent using this endpoint. These events will be - * overwritten if ````, ```` and ```` all + * overwritten if ``, `` and `` all * match. * * Requests to this endpoint **cannot use transaction IDs** - * like other ``PUT`` paths because they cannot be differentiated from the - * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. + * like other `PUT` paths because they cannot be differentiated from the + * `state_key`. Furthermore, `POST` is unsupported on state paths. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the ``m.`` event specification. + * [Room Events](/client-server-api/#room-events) for the `m.` event + * specification. * - * If the event type being sent is ``m.room.canonical_alias`` servers + * If the event type being sent is `m.room.canonical_alias` servers * SHOULD ensure that any new aliases being listed in the event are valid * per their grammar/syntax and that they point to the room ID where the * state event is to be sent. Servers do not validate aliases which are @@ -75,7 +71,10 @@ public: // Result properties /// A unique identifier for the event. - QString eventId() const { return loadFromJson("event_id"_ls); } + QString eventId() const + { + return loadFromJson("event_id"_ls); + } }; } // namespace Quotient diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index f0bfa349..179d7a27 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -12,7 +12,7 @@ namespace Quotient { /*! \brief Get a single event by event ID. * - * Get a single event based on ``roomId/eventId``. You must have permission to + * Get a single event based on `roomId/eventId`. You must have permission to * retrieve this event e.g. by being a member in the room for this event. */ class GetOneRoomEventJob : public BaseJob { @@ -38,13 +38,14 @@ public: // Result properties /// The full event. - EventPtr event() { return fromJson(jsonData()); } + EventPtr event() + + { + return fromJson(jsonData()); + } }; /*! \brief Get the state identified by the type and key. - * - * .. For backwards compatibility with older links... - * .. _`get-matrix-client-r0-rooms-roomid-state-eventtype`: * * Looks up the contents of a state event in a room. If the user is * joined to the room then the state is taken from the current @@ -102,7 +103,11 @@ public: // Result properties /// The current state of the room - StateEvents events() { return fromJson(jsonData()); } + StateEvents events() + + { + return fromJson(jsonData()); + } }; /*! \brief Get the m.room.member events for the room. @@ -118,16 +123,15 @@ public: * * \param at * The point in time (pagination token) to return members for in the room. - * This token can be obtained from a ``prev_batch`` token returned for + * This token can be obtained from a `prev_batch` token returned for * each room by the sync API. Defaults to the current state of the room, * as determined by the server. * * \param membership * The kind of membership to filter for. Defaults to no filtering if - * unspecified. When specified alongside ``not_membership``, the two + * unspecified. When specified alongside `not_membership`, the two * parameters create an 'or' condition: either the membership *is* - * the same as ``membership`` **or** *is not* the same as - * ``not_membership``. + * the same as `membership` **or** *is not* the same as `not_membership`. * * \param notMembership * The kind of membership to exclude from the results. Defaults to no @@ -162,7 +166,7 @@ public: * room. The current user must be in the room for it to work, unless it is an * Application Service in which case any of the AS's users must be in the room. * This API is primarily for Application Services and should be faster to - * respond than ``/members`` as it can be implemented more efficiently on the + * respond than `/members` as it can be implemented more efficiently on the * server. */ class GetJoinedMembersByRoomJob : public BaseJob { @@ -173,7 +177,7 @@ public: /// the room. The current user must be in the room for it to work, unless it /// is an Application Service in which case any of the AS's users must be in /// the room. This API is primarily for Application Services and should be - /// faster to respond than ``/members`` as it can be implemented more + /// faster to respond than `/members` as it can be implemented more /// efficiently on the server. struct RoomMember { /// The display name of the user this object is representing. diff --git a/lib/csapi/search.h b/lib/csapi/search.h index c009ded6..b56d9154 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -23,15 +23,15 @@ public: /// returned are included in the response. struct IncludeEventContext { /// How many events before the result are - /// returned. By default, this is ``5``. + /// returned. By default, this is `5`. Omittable beforeLimit; /// How many events after the result are - /// returned. By default, this is ``5``. + /// returned. By default, this is `5`. Omittable afterLimit; /// Requests that the server returns the /// historic profile information for the users /// that sent the events that were returned. - /// By default, this is ``false``. + /// By default, this is `false`. Omittable includeProfile; }; @@ -54,10 +54,10 @@ public: QString searchTerm; /// The keys to search. Defaults to all. QStringList keys; - /// This takes a `filter`_. + /// This takes a [filter](/client-server-api/#filtering). RoomEventFilter filter; /// The order in which to search for results. - /// By default, this is ``"rank"``. + /// By default, this is `"rank"`. QString orderBy; /// Configures whether any context for the events /// returned are included in the response. @@ -93,7 +93,7 @@ public: /// The historic profile information of the /// users that sent the events returned. /// - /// The ``string`` key is the user ID for which + /// The `string` key is the user ID for which /// the profile belongs to. QHash profileInfo; /// Events just before the result. @@ -139,15 +139,15 @@ public: std::vector results; /// The current state for every room in the results. /// This is included if the request had the - /// ``include_state`` key set with a value of ``true``. + /// `include_state` key set with a value of `true`. /// - /// The ``string`` key is the room ID for which the ``State - /// Event`` array belongs to. + /// The `string` key is the room ID for which the `State + /// Event` array belongs to. UnorderedMap state; /// Any groups that were requested. /// - /// The outer ``string`` key is the group key requested (eg: ``room_id`` - /// or ``sender``). The inner ``string`` key is the grouped value (eg: + /// The outer `string` key is the group key requested (eg: `room_id` + /// or `sender`). The inner `string` key is the grouped value (eg: /// a room's ID or a user's ID). QHash> groups; /// Token that can be used to get the next batch of @@ -172,7 +172,7 @@ public: * * \param nextBatch * The point to return events from. If given, this should be a - * ``next_batch`` result from a previous call to this endpoint. + * `next_batch` result from a previous call to this endpoint. */ explicit SearchJob(const Categories& searchCategories, const QString& nextBatch = {}); diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index 85a18560..e3d39807 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -28,3 +28,27 @@ RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect", queryToRedirectToSSO(redirectUrl), {}, false) {} + +auto queryToRedirectToIdP(const QString& redirectUrl) +{ + BaseJob::Query _q; + addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); + return _q; +} + +QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, + const QString& redirectUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + QStringLiteral("/_matrix/client/r0") + % "/login/sso/redirect/" % idpId, + queryToRedirectToIdP(redirectUrl)); +} + +RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, + const QString& redirectUrl) + : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), + QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect/" + % idpId, + queryToRedirectToIdP(redirectUrl), {}, false) +{} diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h index d6330e38..ade1eb7d 100644 --- a/lib/csapi/sso_login_redirect.h +++ b/lib/csapi/sso_login_redirect.h @@ -13,7 +13,9 @@ namespace Quotient { * A web-based Matrix client should instruct the user's browser to * navigate to this endpoint in order to log in via SSO. * - * The server MUST respond with an HTTP redirect to the SSO interface. + * The server MUST respond with an HTTP redirect to the SSO interface, + * or present a page which lets the user select an IdP to continue + * with in the event multiple are supported by the server. */ class RedirectToSSOJob : public BaseJob { public: @@ -33,4 +35,36 @@ public: static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); }; +/*! \brief Redirect the user's browser to the SSO interface for an IdP. + * + * This endpoint is the same as `/login/sso/redirect`, though with an + * IdP ID from the original `identity_providers` array to inform the + * server of which IdP the client/user would like to continue with. + * + * The server MUST respond with an HTTP redirect to the SSO interface + * for that IdP. + */ +class RedirectToIdPJob : public BaseJob { +public: + /*! \brief Redirect the user's browser to the SSO interface for an IdP. + * + * \param idpId + * The `id` of the IdP from the `m.login.sso` `identity_providers` + * array denoting the user's selection. + * + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToIdPJob(const QString& idpId, const QString& redirectUrl); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RedirectToIdPJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& idpId, + const QString& redirectUrl); +}; + } // namespace Quotient diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index a815d9b3..a854531a 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -18,7 +18,7 @@ public: /// List the tags set by a user on a room. struct Tag { - /// A number in a range ``[0,1]`` describing a relative + /// A number in a range `[0,1]` describing a relative /// position of the room under the given tag. Omittable order; /// List the tags set by a user on a room. @@ -83,7 +83,7 @@ public: * The tag to add. * * \param order - * A number in a range ``[0,1]`` describing a relative + * A number in a range `[0,1]` describing a relative * position of the room under the given tag. * * \param additionalProperties diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index 55cab370..a424678f 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -9,15 +9,14 @@ namespace Quotient { /*! \brief Invite a user to participate in a particular room. - * - * .. _invite-by-third-party-id-endpoint: * * *Note that there are two forms of this API, which are documented separately. * This version of the API does not require that the inviter know the Matrix * identifier of the invitee, and instead relies on third party identifiers. * The homeserver uses an identity server to perform the mapping from * third party identifier to a Matrix identifier. The other is documented in - * the* `joining rooms section`_. + * the* [joining rooms + * section](/client-server-api/#post_matrixclientr0roomsroomidinvite). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the @@ -27,7 +26,7 @@ namespace Quotient { * join that room. * * If the identity server did know the Matrix user identifier for the - * third party identifier, the homeserver will append a ``m.room.member`` + * third party identifier, the homeserver will append a `m.room.member` * event to the room. * * If the identity server does not know a Matrix user identifier for the @@ -35,7 +34,7 @@ namespace Quotient { * which can be accepted upon providing proof of ownership of the third * party identifier. This is achieved by the identity server generating a * token, which it gives to the inviting homeserver. The homeserver will - * add an ``m.room.third_party_invite`` event into the graph for the room, + * add an `m.room.third_party_invite` event into the graph for the room, * containing that token. * * When the invitee binds the invited third party identifier to a Matrix @@ -51,9 +50,7 @@ namespace Quotient { * - The matrix user ID who invited them to the room * * If a token is requested from the identity server, the homeserver will - * append a ``m.room.third_party_invite`` event to the room. - * - * .. _joining rooms section: `invite-by-user-id-endpoint`_ + * append a `m.room.third_party_invite` event to the room. */ class InviteBy3PIDJob : public BaseJob { public: @@ -73,7 +70,7 @@ public: * * \param medium * The kind of address being passed in the address field, for example - * ``email``. + * `email`. * * \param address * The invitee's third party identifier. diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 28c4115a..3775174d 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -16,6 +16,6 @@ SendToDeviceJob::SendToDeviceJob( % eventType % "/" % txnId) { QJsonObject _data; - addParam(_data, QStringLiteral("messages"), messages); + addParam<>(_data, QStringLiteral("messages"), messages); setRequestData(std::move(_data)); } diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index f5d69d65..7a237195 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -32,7 +32,7 @@ public: */ explicit SendToDeviceJob( const QString& eventType, const QString& txnId, - const QHash>& messages = {}); + const QHash>& messages); }; } // namespace Quotient diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index 2c953949..64a310d0 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -11,8 +11,8 @@ namespace Quotient { /*! \brief Informs the server that the user has started or stopped typing. * * This tells the server that the user is typing for the next N - * milliseconds where N is the value specified in the ``timeout`` key. - * Alternatively, if ``typing`` is ``false``, it tells the server that the + * milliseconds where N is the value specified in the `timeout` key. + * Alternatively, if `typing` is `false`, it tells the server that the * user has stopped typing. */ class SetTypingJob : public BaseJob { @@ -26,7 +26,7 @@ public: * The room in which the user is typing. * * \param typing - * Whether the user is typing or not. If ``false``, the ``timeout`` + * Whether the user is typing or not. If `false`, the `timeout` * key can be omitted. * * \param timeout diff --git a/lib/csapi/users.h b/lib/csapi/users.h index 6fc26f57..772a6365 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -19,7 +19,7 @@ namespace Quotient { * * The search is performed case-insensitively on user IDs and display * names preferably using a collation determined based upon the - * ``Accept-Language`` header provided in the request, if present. + * `Accept-Language` header provided in the request, if present. */ class SearchUserDirectoryJob : public BaseJob { public: @@ -34,7 +34,7 @@ public: /// /// The search is performed case-insensitively on user IDs and display /// names preferably using a collation determined based upon the - /// ``Accept-Language`` header provided in the request, if present. + /// `Accept-Language` header provided in the request, if present. struct User { /// The user's matrix user ID. QString userId; @@ -66,7 +66,10 @@ public: } /// Indicates if the result list has been truncated by the limit. - bool limited() const { return loadFromJson("limited"_ls); } + bool limited() const + { + return loadFromJson("limited"_ls); + } }; template <> diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 828a7eb9..896e2ea9 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -12,14 +12,14 @@ namespace Quotient { * * Gets the versions of the specification supported by the server. * - * Values will take the form ``rX.Y.Z``. + * Values will take the form `rX.Y.Z`. * - * Only the latest ``Z`` value will be reported for each supported ``X.Y`` - * value. i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, - * it will report ``r0.0.1`` and ``r1.2.0``. + * Only the latest `Z` value will be reported for each supported `X.Y` value. + * i.e. if the server implements `r0.0.0`, `r0.0.1`, and `r1.2.0`, it will + * report `r0.0.1` and `r1.2.0`. * * The server may additionally advertise experimental features it supports - * through ``unstable_features``. These features should be namespaced and + * through `unstable_features`. These features should be namespaced and * may optionally include version information within their name if desired. * Features listed here are not for optionally toggling parts of the Matrix * specification and should only be used to advertise support for a feature diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index 087ebbbd..85ab8b41 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -28,7 +28,10 @@ public: // Result properties /// The TURN server credentials. - QJsonObject data() const { return fromJson(jsonData()); } + QJsonObject data() const + { + return fromJson(jsonData()); + } }; } // namespace Quotient diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index b21d9fc7..c707d232 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -14,7 +14,7 @@ namespace Quotient { * * Gets discovery information about the domain. The file may include * additional keys, which MUST follow the Java package naming convention, - * e.g. ``com.example.myapp.property``. This ensures property names are + * e.g. `com.example.myapp.property`. This ensures property names are * suitably namespaced for each application and reduces the risk of * clashes. * diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index af8f1e8a..203742c9 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -14,8 +14,8 @@ namespace Quotient { * * Note that, as with the rest of the Client-Server API, * Application Services may masquerade as users within their - * namespace by giving a ``user_id`` query parameter. In this - * situation, the server should verify that the given ``user_id`` + * namespace by giving a `user_id` query parameter. In this + * situation, the server should verify that the given `user_id` * is registered by the appservice, and return it in the response * body. */ @@ -33,8 +33,20 @@ public: // Result properties - /// The user id that owns the access token. - QString userId() const { return loadFromJson("user_id"_ls); } + /// The user ID that owns the access token. + QString userId() const + { + return loadFromJson("user_id"_ls); + } + + /// Device ID associated with the access token. If no device + /// is associated with the access token (such as in the case + /// of application services) then this field can be omitted. + /// Otherwise this is required. + QString deviceId() const + { + return loadFromJson("device_id"_ls); + } }; } // namespace Quotient diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index 079da953..87549505 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -11,16 +11,16 @@ namespace Quotient { struct RequestEmailValidation { /// A unique string generated by the client, and used to identify the /// validation attempt. It must be a string consisting of the characters - /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it + /// `[0-9a-zA-Z.=_-]`. Its length must not exceed 255 characters and it /// must not be empty. QString clientSecret; /// The email address to validate. QString email; - /// The server will only send an email if the ``send_attempt`` + /// The server will only send an email if the `send_attempt` /// is a number greater than the most recent one which it has seen, - /// scoped to that ``email`` + ``client_secret`` pair. This is to + /// scoped to that `email` + `client_secret` pair. This is to /// avoid repeatedly sending the same email in the case of request /// retries between the POSTing user and the identity server. /// The client should increment this value if they desire a new diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index a29fd0de..d2ea463f 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -11,20 +11,20 @@ namespace Quotient { struct RequestMsisdnValidation { /// A unique string generated by the client, and used to identify the /// validation attempt. It must be a string consisting of the characters - /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it + /// `[0-9a-zA-Z.=_-]`. Its length must not exceed 255 characters and it /// must not be empty. QString clientSecret; /// The two-letter uppercase ISO-3166-1 alpha-2 country code that the - /// number in ``phone_number`` should be parsed as if it were dialled from. + /// number in `phone_number` should be parsed as if it were dialled from. QString country; /// The phone number to validate. QString phoneNumber; - /// The server will only send an SMS if the ``send_attempt`` is a + /// The server will only send an SMS if the `send_attempt` is a /// number greater than the most recent one which it has seen, - /// scoped to that ``country`` + ``phone_number`` + ``client_secret`` + /// scoped to that `country` + `phone_number` + `client_secret` /// triple. This is to avoid repeatedly sending the same SMS in /// the case of request retries between the POSTing user and the /// identity server. The client should increment this value if -- cgit v1.2.3 From 466fe1a43389ac1d7408d32eee3e85e8b074b4bd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 23 Jun 2021 20:16:04 +0200 Subject: Step to Ubuntu 20.04; drop (old) E2EE code building --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed251bc3..20100e5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,17 +16,15 @@ jobs: fail-fast: false max-parallel: 1 matrix: - os: [ubuntu-18.04, macos-10.15] + os: [ubuntu-20.04, macos-10.15] compiler: [ GCC, Clang ] qt-version: [ '5.12.10' ] # Not using binary values here, to make the job captions more readable - e2ee: [ '', 'E2EE' ] + e2ee: [ '' ] update-api: [ '', 'update-api' ] exclude: - os: macos-10.15 compiler: GCC - - e2ee: '' # Somewhat reduce the number of combinations to check - update-api: 'update-api' steps: - uses: actions/checkout@v2 -- cgit v1.2.3 From ac8a4487e0254386462c43bfc0aeef8412854117 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 28 Jun 2021 20:32:14 +0200 Subject: gtad.yaml: update for use with GTAD pre-0.8 --- gtad/gtad.yaml | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index d68cc8a0..e56c394d 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -77,28 +77,31 @@ analyzer: +on: - object: &QJsonObject { type: QJsonObject } - $ref: - - +set: { moveOnly: } + - +set: + moveOnly: + imports: '"events/eventloader.h"' +on: - - /state_event.yaml$/: - { type: StateEventPtr, imports: "events/eventloader.h" } - - /room_event.yaml$/: - { type: RoomEventPtr, imports: "events/eventloader.h" } - - /event.yaml$/: - { type: EventPtr, imports: "events/eventloader.h" } + - /state_event.yaml$/: StateEventPtr + - /room_event.yaml$/: RoomEventPtr + - /event.yaml$/: EventPtr - /m\.room\.member/: void # Skip resolving; see EventsArray<> below - - '/^(\./)?definitions/request_email_validation.yaml$/': - title: EmailValidationData - - '/^(\./)?definitions/request_msisdn_validation.yaml$/': - title: MsisdnValidationData - - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<> - - /public_rooms_response.yaml$/: { _inline: true } - - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types + - +set: + # This renderer actually applies to all $ref things + _importRenderer: '"{{#segments}}{{_}}{{#_join}}/{{/_join}}{{/segments}}.h"' + +on: + - '/^(\./)?definitions/request_email_validation.yaml$/': + title: EmailValidationData + - '/^(\./)?definitions/request_msisdn_validation.yaml$/': + title: MsisdnValidationData + - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<> + - /public_rooms_response.yaml$/: { _inline: true } + - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types - schema: - getTurnServer<: *QJsonObject # It's used as an opaque JSON object - PublicRoomResponse: { _inline: true } # - defineFilter>: &Filter # Force folding into a structure # type: Filter -# imports: "csapi/definitions/sync_filter.h" +# imports: '"csapi/definitions/sync_filter.h"' # - getFilter<: *Filter - RoomFilter: # A structure inside Filter, same story as with *_filter.yaml - //: *UseOmittable @@ -108,10 +111,10 @@ analyzer: +on: - /^Notification|Result$/: type: "std::vector<{{1}}>" - imports: "events/eventloader.h" + imports: '"events/eventloader.h"' - /m\.room\.member/: # Only used in an array (see also above) type: "EventsArray" - imports: "events/roommemberevent.h" + imports: '"events/roommemberevent.h"' - /state_event.yaml$/: StateEvents - /room_event.yaml$/: RoomEvents - /event.yaml$/: Events @@ -134,7 +137,6 @@ mustache: # _quote: '"' # Common quote for left and right # _leftQuote: '"' # _rightQuote: '"' -# _joinChar: ',' # The character used by {{_join}} - not working yet _comment: '//' copyrightName: Kitsune Ral copyrightEmail: -- cgit v1.2.3 From 7f4ef8b303c36831a9172a1fc60888e906fc9b93 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 30 Jun 2021 20:10:40 +0200 Subject: Add a commented out delimiter override example --- gtad/gtad.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index e56c394d..928a1495 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -132,6 +132,7 @@ analyzer: #operations: mustache: +# delimiter: '%| |%' # or something else instead of '{{ }}' constants: # Syntax elements used by GTAD # _quote: '"' # Common quote for left and right -- cgit v1.2.3 From b64bfa8f72084d9d9397001a735e985a4bf94e56 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 2 Jul 2021 10:33:59 +0200 Subject: Abandon BaseJob::Query - Mustache template --- gtad/operation.cpp.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache index 5bbc45ec..f34c9280 100644 --- a/gtad/operation.cpp.mustache +++ b/gtad/operation.cpp.mustache @@ -13,7 +13,7 @@ using namespace Quotient; auto queryTo{{camelCaseOperationId}}( {{#queryParams}}{{>joinedParamDef}}{{/queryParams}}) { - BaseJob::Query _q;{{#queryParams}} + QUrlQuery _q;{{#queryParams}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(_q, QStringLiteral("{{baseName}}"), {{paramName}});{{/queryParams}} return _q; -- cgit v1.2.3 From 7aa6e7c300779f652558397bcb7bb3b726d30cb9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 2 Jul 2021 10:34:16 +0200 Subject: Abandon BaseJob::Query - generated API files --- lib/csapi/content-repo.cpp | 10 +++++----- lib/csapi/event_context.cpp | 2 +- lib/csapi/joining.cpp | 2 +- lib/csapi/keys.cpp | 2 +- lib/csapi/knocking.cpp | 2 +- lib/csapi/list_public_rooms.cpp | 4 ++-- lib/csapi/message_pagination.cpp | 2 +- lib/csapi/notifications.cpp | 2 +- lib/csapi/peeking_events.cpp | 2 +- lib/csapi/pushrules.cpp | 2 +- lib/csapi/registration.cpp | 4 ++-- lib/csapi/rooms.cpp | 2 +- lib/csapi/search.cpp | 2 +- lib/csapi/sso_login_redirect.cpp | 4 ++-- lib/csapi/third_party_lookup.cpp | 8 ++++---- 15 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 7ae89739..e913bfd1 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -10,7 +10,7 @@ using namespace Quotient; auto queryToUploadContent(const QString& filename) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("filename"), filename); return _q; } @@ -28,7 +28,7 @@ UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, auto queryToGetContent(bool allowRemote) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } @@ -55,7 +55,7 @@ GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, auto queryToGetContentOverrideName(bool allowRemote) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } @@ -88,7 +88,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, auto queryToGetContentThumbnail(int width, int height, const QString& method, bool allowRemote) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("width"), width); addParam<>(_q, QStringLiteral("height"), height); addParam(_q, QStringLiteral("method"), method); @@ -124,7 +124,7 @@ GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, auto queryToGetUrlPreview(const QString& url, Omittable ts) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("url"), url); addParam(_q, QStringLiteral("ts"), ts); return _q; diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index d2a5f522..3f4cd61e 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -10,7 +10,7 @@ using namespace Quotient; auto queryToGetEventContext(Omittable limit, const QString& filter) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("limit"), limit); addParam(_q, QStringLiteral("filter"), filter); return _q; diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 998d0b42..f5266f0b 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -24,7 +24,7 @@ JoinRoomByIdJob::JoinRoomByIdJob( auto queryToJoinRoom(const QStringList& serverName) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("server_name"), serverName); return _q; } diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index 34ab47c9..ba5d8e12 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -47,7 +47,7 @@ ClaimKeysJob::ClaimKeysJob( auto queryToGetKeysChanges(const QString& from, const QString& to) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("from"), from); addParam<>(_q, QStringLiteral("to"), to); return _q; diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp index 4b561300..788bb378 100644 --- a/lib/csapi/knocking.cpp +++ b/lib/csapi/knocking.cpp @@ -10,7 +10,7 @@ using namespace Quotient; auto queryToKnockRoom(const QStringList& serverName) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("server_name"), serverName); return _q; } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 415d816c..a4bcb934 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -38,7 +38,7 @@ SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( auto queryToGetPublicRooms(Omittable limit, const QString& since, const QString& server) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("limit"), limit); addParam(_q, QStringLiteral("since"), since); addParam(_q, QStringLiteral("server"), server); @@ -66,7 +66,7 @@ GetPublicRoomsJob::GetPublicRoomsJob(Omittable limit, const QString& since, auto queryToQueryPublicRooms(const QString& server) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("server"), server); return _q; } diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 855c051f..441e4dea 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -12,7 +12,7 @@ auto queryToGetRoomEvents(const QString& from, const QString& to, const QString& dir, Omittable limit, const QString& filter) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index a479d500..a38e46f5 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -11,7 +11,7 @@ using namespace Quotient; auto queryToGetNotifications(const QString& from, Omittable limit, const QString& only) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("limit"), limit); addParam(_q, QStringLiteral("only"), only); diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index 70a5b6f3..ad2f9afe 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -11,7 +11,7 @@ using namespace Quotient; auto queryToPeekEvents(const QString& from, Omittable timeout, const QString& roomId) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("timeout"), timeout); addParam(_q, QStringLiteral("room_id"), roomId); diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index 86165744..ab7d0038 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -57,7 +57,7 @@ DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, auto queryToSetPushRule(const QString& before, const QString& after) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("before"), before); addParam(_q, QStringLiteral("after"), after); return _q; diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 33f61265..38649e63 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -10,7 +10,7 @@ using namespace Quotient; auto queryToRegister(const QString& kind) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("kind"), kind); return _q; } @@ -106,7 +106,7 @@ DeactivateAccountJob::DeactivateAccountJob( auto queryToCheckUsernameAvailability(const QString& username) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("username"), username); return _q; } diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 724d941f..3dd87021 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -58,7 +58,7 @@ GetRoomStateJob::GetRoomStateJob(const QString& roomId) auto queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("at"), at); addParam(_q, QStringLiteral("membership"), membership); addParam(_q, QStringLiteral("not_membership"), notMembership); diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 5649d52a..05ad871e 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -10,7 +10,7 @@ using namespace Quotient; auto queryToSearch(const QString& nextBatch) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("next_batch"), nextBatch); return _q; } diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index e3d39807..92601b4d 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -10,7 +10,7 @@ using namespace Quotient; auto queryToRedirectToSSO(const QString& redirectUrl) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); return _q; } @@ -31,7 +31,7 @@ RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) auto queryToRedirectToIdP(const QString& redirectUrl) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); return _q; } diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index baf1fab5..93687a76 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -36,7 +36,7 @@ GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) auto queryToQueryLocationByProtocol(const QString& searchFields) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("searchFields"), searchFields); return _q; } @@ -61,7 +61,7 @@ QueryLocationByProtocolJob::QueryLocationByProtocolJob( auto queryToQueryUserByProtocol(const QString& fields) { - BaseJob::Query _q; + QUrlQuery _q; addParam(_q, QStringLiteral("fields..."), fields); return _q; } @@ -86,7 +86,7 @@ QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, auto queryToQueryLocationByAlias(const QString& alias) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("alias"), alias); return _q; } @@ -107,7 +107,7 @@ QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) auto queryToQueryUserByID(const QString& userid) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("userid"), userid); return _q; } -- cgit v1.2.3 From 74dda96855b6245b09a8f7eb0512b4457b18a6b7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 2 Jul 2021 10:35:45 +0200 Subject: Actually delete BaseJob::Query It was a tiny wrapper around QUrlQuery to facilitate creation from an initializer list - however, Mustache templates long changed to not actually used that additional constructor. --- lib/jobs/basejob.cpp | 4 ++-- lib/jobs/basejob.h | 16 +--------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index c27c6a89..9a7b9b5e 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -175,11 +175,11 @@ public: BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken) + : BaseJob(verb, name, endpoint, QUrlQuery {}, Data {}, needsToken) {} BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const Query& query, Data&& data, bool needsToken) + const QUrlQuery &query, Data&& data, bool needsToken) : d(new Private(verb, endpoint, query, std::move(data), needsToken)) { setObjectName(name); diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index ca91a781..d33d542e 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -72,20 +72,6 @@ public: }; Q_ENUM(StatusCode) - /** - * A simple wrapper around QUrlQuery that allows its creation from - * a list of string pairs - */ - class Query : public QUrlQuery { - public: - using QUrlQuery::QUrlQuery; - Query() = default; - Query(const std::initializer_list>& l) - { - setQueryItems(l); - } - }; - using Data = RequestData; /*! @@ -139,7 +125,7 @@ public: BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken = true); BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const Query& query, Data&& data = {}, bool needsToken = true); + const QUrlQuery& query, Data&& data = {}, bool needsToken = true); QUrl requestUrl() const; bool isBackground() const; -- 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(-) 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 9a5fa623c17f5644da7cdb459ade86bc8e1cdbf3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 16 Jul 2021 20:02:00 +0200 Subject: Officially drop Qt Multimedia with Qt 6 Closes #483. --- CMakeLists.txt | 14 +++++++------- lib/events/roommessageevent.cpp | 6 ++---- lib/events/roommessageevent.h | 4 ++++ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 555ffa96..285862df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,14 +75,14 @@ set(CMAKE_AUTOMOC ON) option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF) if (BUILD_WITH_QT6) - find_package(Qt6 6.2 REQUIRED Core Network Gui Test) # TODO: Multimedia - set(Qt Qt6) - qt6_wrap_cpp(lib_SRCS lib/quotient_common.h) + set(QtMinVersion "6.0") else() - find_package(Qt5 5.12 REQUIRED Core Network Gui Multimedia Test) - set(Qt Qt5) + set(QtMinVersion "5.12") + set(QtExtraModules "Multimedia") # See #483 endif() -get_filename_component($Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) +string(REGEX REPLACE "^(.).*" "Qt\\1" Qt ${QtMinVersion}) # makes "Qt5" or "Qt6" +find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModules}) +get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") if (${PROJECT_NAME}_ENABLE_E2EE) @@ -299,7 +299,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in endif() target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) -if (Qt STREQUAL Qt5) # Qt 6 hasn't got Multimedia component as yet +if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) endif() diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 3f6e475d..71f85363 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -135,6 +135,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType, : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) {} +#if QT_VERSION_MAJOR < 6 TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) { auto filePath = file.absoluteFilePath(); @@ -151,11 +152,7 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, -#if QT_VERSION_MAJOR < 6 QMediaResource(localUrl).resolution(), -#else - {}, -#endif file.fileName()); if (mimeTypeName.startsWith("audio/")) @@ -172,6 +169,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : rawMsgTypeForFile(file), contentFromFile(file, asGenericFile)) {} +#endif RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 8303ce4e..7bcda2ba 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -42,8 +42,12 @@ public: explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); +#if QT_VERSION_MAJOR < 6 + [[deprecated("Create an EventContent object on the client side" + " and pass it to other constructors")]] // explicit RoomMessageEvent(const QString& plainBody, const QFileInfo& file, bool asGenericFile = false); +#endif explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; -- cgit v1.2.3 From 0cb9a462945b1a9dd30979e9b97415dff16816e0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 16 Jul 2021 20:03:49 +0200 Subject: UriResolver: fix clang-tidy warnings --- lib/uriresolver.cpp | 2 ++ lib/uriresolver.h | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp index 287e0552..681e3842 100644 --- a/lib/uriresolver.cpp +++ b/lib/uriresolver.cpp @@ -8,6 +8,8 @@ using namespace Quotient; +UriResolverBase::~UriResolverBase() = default; + UriResolveResult UriResolverBase::visitResource(Connection* account, const Uri& uri) { diff --git a/lib/uriresolver.h b/lib/uriresolver.h index f290e58b..ff97324a 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -42,23 +42,29 @@ public: UriResolveResult visitResource(Connection* account, const Uri& uri); protected: + virtual ~UriResolverBase() = 0; + /// Called by visitResource() when the passed URI identifies a Matrix user /*! * \return IncorrectAction if the action is not correct or not supported; * UriResolved if it is accepted; other values are disallowed */ - virtual UriResolveResult visitUser(User* user, const QString& action) + virtual UriResolveResult visitUser(User* user [[maybe_unused]], + const QString& action [[maybe_unused]]) { return IncorrectAction; } /// Called by visitResource() when the passed URI identifies a room or /// an event in a room - virtual void visitRoom(Room* room, const QString& eventId) {} + virtual void visitRoom(Room* room [[maybe_unused]], + const QString& eventId [[maybe_unused]]) + {} /// Called by visitResource() when the passed URI has `action() == "join"` /// and identifies a room that the user defined by the Connection argument /// is not a member of - virtual void joinRoom(Connection* account, const QString& roomAliasOrId, - const QStringList& viaServers = {}) + virtual void joinRoom(Connection* account [[maybe_unused]], + const QString& roomAliasOrId [[maybe_unused]], + const QStringList& viaServers [[maybe_unused]] = {}) {} /// Called by visitResource() when the passed URI has `type() == NonMatrix` /*! @@ -67,7 +73,10 @@ protected: * `return QDesktopServices::openUrl(url);` but it's strongly advised to * ask for a user confirmation beforehand. */ - virtual bool visitNonMatrix(const QUrl& url) { return false; } + virtual bool visitNonMatrix(const QUrl& url [[maybe_unused]]) + { + return false; + } }; /*! \brief Resolve the resource and invoke an action on it, via function objects -- cgit v1.2.3 From e9dace0e22a2aa3727bc842a30b4ad504029c0fc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 16 Jul 2021 20:09:42 +0200 Subject: Log thumbnail requests in their own category As pointed out by one of users, thumbnail requests produce quite a bit of logging traffic, so it's better to manage them separately. --- README.md | 10 +++++----- lib/jobs/mediathumbnailjob.cpp | 8 ++++++-- lib/logging.cpp | 1 + lib/logging.h | 1 + 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c993c31e..d5aae543 100644 --- a/README.md +++ b/README.md @@ -196,11 +196,11 @@ libQuotient uses Qt's logging categories to make switching certain types of logg quotient..= ``` where -- `` is one of: `main`, `jobs`, `jobs.sync`, `events`, `events.state` - (covering both the "usual" room state and account data), `events.messages`, - `events.ephemeral`, `e2ee` and `profiler` (you can always find the full list - in `lib/logging.cpp`) -- `` is one of `debug`, `info`, and `warning` +- `` is one of: `main`, `jobs`, `jobs.sync`, `jobs.thumbnail`, + `events`, `events.state` (covering both the "usual" room state and account + data), `events.messages`, `events.ephemeral`, `e2ee` and `profiler` (you can + always find the full list in `lib/logging.cpp`); +- `` is one of `debug`, `info`, and `warning`; - `` is either `true` or `false`. `*` can be used as a wildcard for any part between two dots, and semicolon is used for a separator. Latter statements override former ones, so if you want to switch on all debug logs except `jobs` you can set diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index 7dbf4ab3..6fe8ef26 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -17,13 +17,17 @@ MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, const QString& mediaId, QSize requestedSize) : GetContentThumbnailJob(serverName, mediaId, requestedSize.width(), requestedSize.height(), "scale") -{} +{ + setLoggingCategory(THUMBNAILJOB); +} MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/' requestedSize) -{} +{ + setLoggingCategory(THUMBNAILJOB); +} QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; } diff --git a/lib/logging.cpp b/lib/logging.cpp index af229684..ffcc851c 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -16,4 +16,5 @@ LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") LOGGING_CATEGORY(E2EE, "quotient.e2ee") LOGGING_CATEGORY(JOBS, "quotient.jobs") LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") +LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") LOGGING_CATEGORY(PROFILER, "quotient.profiler") diff --git a/lib/logging.h b/lib/logging.h index 432ed16f..264215e1 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -16,6 +16,7 @@ Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) Q_DECLARE_LOGGING_CATEGORY(E2EE) Q_DECLARE_LOGGING_CATEGORY(JOBS) Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) +Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB) Q_DECLARE_LOGGING_CATEGORY(PROFILER) namespace Quotient { -- cgit v1.2.3 From 680d51c4686ab5645405a0ca9631b71a567eaefa Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 16 Jul 2021 20:09:59 +0200 Subject: SyncData::parseJson(): minor optimisation --- lib/syncdata.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index adcba5cd..232f3694 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -171,13 +171,19 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) deviceOneTimeKeysCount_); auto rooms = json.value("rooms"_ls).toObject(); - JoinStates::Int ii = 1; // ii is used to make a JoinState value auto totalRooms = 0; auto totalEvents = 0; - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) { + // The first comparison shortcuts the loop when not all states are there + // in the response (anything except "join" is only occasional, and "join" + // intentionally comes first in the enum). + for (size_t i = 0; + static_cast(i) < rooms.size() && i < JoinStateStrings.size(); ++i) + { + // This assumes that MemberState values go over powers of 2: 1,2,4,... + const auto joinState = JoinState(1U << i); const auto rs = rooms.value(JoinStateStrings[i]).toObject(); // We have a Qt container on the right and an STL one on the left - roomData.reserve(static_cast(rs.size())); + roomData.reserve(roomData.size() + static_cast(rs.size())); for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { auto roomJson = roomIt->isObject() @@ -187,7 +193,7 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) unresolvedRoomIds.push_back(roomIt.key()); continue; } - roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); + roomData.emplace_back(roomIt.key(), joinState, roomJson); const auto& r = roomData.back(); totalEvents += r.state.size() + r.ephemeral.size() + r.accountData.size() + r.timeline.size(); -- cgit v1.2.3 From d85c63ffaf776683a74d0a20dcdb76f7ade8461b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 16 Jul 2021 22:11:27 +0200 Subject: User::rename(): don't discard the current state Closes #481. --- lib/user.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/user.cpp b/lib/user.cpp index 7933c5d9..6cc9e161 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -121,11 +121,11 @@ void User::rename(const QString& newName, const Room* r) rename(newName); return; } - Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, + // #481: take the current state and update it with the new name + auto evtC = r->getCurrentState(id())->content(); + Q_ASSERT_X(evtC.membership == MembershipType::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); - const auto actualNewName = sanitized(newName); - MemberEventContent evtC; - evtC.displayName = actualNewName; + evtC.displayName = sanitized(newName); r->setState(id(), move(evtC)); // The state will be updated locally after it arrives with sync } -- cgit v1.2.3 From e3bdbc84ec5ada04e436dba9067d902e2c6c030a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 14 Jul 2021 14:28:12 +0200 Subject: CMakeLists: fixed potential linking errors around quotient_common.h quotient_common.h has Q_NAMESPACE but no own compilation unit, and moc was not called on it either - using metaobject data on an enumeration defined in that file leads to a linking error due to sharedMetaObject not being defined. The fix makes so that the file is detected by automoc with the respective definition being generated. Cherry-picked from a83ec900 (0.6.x branch). --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 285862df..deb50aea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,11 @@ endif () # Set up source files list(APPEND lib_SRCS + # This .h is special in that it declares a Q_NAMESPACE but has no .cpp + # where staticMetaObject for that namespace would be defined; passing it + # to add_library (see below) puts it on the automoc radar, producing + # a compilation unit with the needed definition. + lib/quotient_common.h lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp -- 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/events/eventcontent.cpp | 51 +++++++++++++++------ lib/events/eventcontent.h | 23 ++++++---- lib/events/roomavatarevent.h | 4 +- lib/room.cpp | 105 ++++++++++++++++++++++++++----------------- lib/room.h | 5 +++ quotest/quotest.cpp | 9 ++-- 6 files changed, 128 insertions(+), 69 deletions(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index b249b160..1f28f195 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -5,10 +5,13 @@ #include "converters.h" #include "util.h" +#include "logging.h" #include +#include using namespace Quotient::EventContent; +using std::move; QJsonObject Base::toJson() const { @@ -17,22 +20,37 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, - const QString& originalFilename) +FileInfo::FileInfo(const QFileInfo &fi) + : mimeType(QMimeDatabase().mimeTypeForFile(fi)) + , url(QUrl::fromLocalFile(fi.filePath())) + , payloadSize(fi.size()) + , originalName(fi.fileName()) +{ + Q_ASSERT(fi.isFile()); +} + +FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, + QString originalFilename) : mimeType(mimeType) - , url(u) + , url(move(u)) , payloadSize(payloadSize) - , originalName(originalFilename) -{} + , originalName(move(originalFilename)) +{ + if (!isValid()) + qCWarning(MESSAGES) + << "To client developers: using FileInfo(QUrl, qint64, ...) " + "constructor for non-mxc resources is deprecated since Quotient " + "0.7; for local resources, use FileInfo(QFileInfo) instead"; +} -FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename) +FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, + QString originalFilename) : originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(u) + , url(move(mxcUrl)) , payloadSize(fromJson(infoJson["size"_ls])) - , originalName(originalFilename) + , originalName(move(originalFilename)) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); @@ -53,14 +71,19 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } -ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, - const QSize& imageSize, const QString& originalFilename) - : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) +ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) + : FileInfo(fi), imageSize(imageSize) +{} + +ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, + QSize imageSize, const QString& originalFilename) + : FileInfo(mxcUrl, fileSize, type, originalFilename) + , imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, +ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, const QString& originalFilename) - : FileInfo(u, infoJson, originalFilename) + : FileInfo(mxcUrl, infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 60d1f7b7..78c5b287 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -10,7 +10,8 @@ #include #include #include -#include + +class QFileInfo; namespace Quotient { namespace EventContent { @@ -73,11 +74,13 @@ namespace EventContent { */ class FileInfo { public: - explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, + FileInfo() = default; + explicit FileInfo(const QFileInfo& fi); + explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); + QString originalFilename = {}); + FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, + QString originalFilename = {}); bool isValid() const; @@ -113,10 +116,12 @@ namespace EventContent { */ class ImageInfo : public FileInfo { public: - explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, - QMimeType mimeType = {}, const QSize& imageSize = {}, + ImageInfo() = default; + explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); + explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, + const QMimeType& type = {}, QSize imageSize = {}, const QString& originalFilename = {}); - ImageInfo(const QUrl& u, const QJsonObject& infoJson, + ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, const QString& originalFilename = {}); void fillInfoJson(QJsonObject* infoJson) const; @@ -134,7 +139,7 @@ namespace EventContent { */ class Thumbnail : public ImageInfo { public: - Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails + Thumbnail() = default; // Allow empty thumbnails Thumbnail(const QJsonObject& infoJson); Thumbnail(const ImageInfo& info) : ImageInfo(info) {} using ImageInfo::ImageInfo; diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index a4257895..3fa11a0f 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -20,12 +20,12 @@ public: : StateEvent(typeId(), matrixTypeId(), QString(), avatar) {} // A replica of EventContent::ImageInfo constructor - explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1, + explicit RoomAvatarEvent(const QUrl& mxcUrl, qint64 fileSize = -1, QMimeType mimeType = {}, const QSize& imageSize = {}, const QString& originalFilename = {}) : RoomAvatarEvent(EventContent::ImageContent { - u, fileSize, mimeType, imageSize, originalFilename }) + mxcUrl, fileSize, mimeType, imageSize, originalFilename }) {} QUrl url() const { return content().url; } 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)); diff --git a/lib/room.h b/lib/room.h index 26d0121e..d71bff9c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -549,8 +549,13 @@ public Q_SLOTS: QString postHtmlText(const QString& plainText, const QString& html); /// Send a reaction on a given event with a given key QString postReaction(const QString& eventId, const QString& key); + + QString postFile(const QString& plainText, EventContent::TypedBase* content); +#if QT_VERSION_MAJOR < 6 + /// \deprecated Use postFile(QString, MessageEventType, EventContent) instead QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); +#endif /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index a0914c72..5646d54d 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -397,15 +397,16 @@ TEST_IMPL(sendFile) } tf->write("Test"); tf->close(); + QFileInfo tfi { *tf }; // QFileInfo::fileName brings only the file name; QFile::fileName brings // the full path - const auto tfName = QFileInfo(*tf).fileName(); + const auto tfName = tfi.fileName(); clog << "Sending file " << tfName.toStdString() << endl; - const auto txnId = - targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName())); + const auto txnId = targetRoom->postFile( + "Test file", new EventContent::FileContent(tfi)); if (!validatePendingEvent(txnId)) { clog << "Invalid pending event right after submitting" << endl; - delete tf; + tf->deleteLater(); FAIL_TEST(); } -- cgit v1.2.3 From 86fe0a8f0682c0122439d53cc96b4b742a69ffcf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 18:24:39 +0200 Subject: Make EventContent::TypedBase() constructor protected TypedBase is an abstract class; constructing it doesn't make sense. But even if it were not abstract, it's not supposed to be instantiated. --- lib/events/eventcontent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 78c5b287..b3a5f280 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -153,13 +153,13 @@ namespace EventContent { class TypedBase : public Base { public: - explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } protected: + explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} using Base::Base; }; -- cgit v1.2.3 From 6d24915e4bdd56dbdace8358297ee9d2d9aa83a0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 18:53:57 +0200 Subject: Revert previous commit Q_DECLARE_METATYPE is really unhappy about types without a public default constructor. --- lib/events/eventcontent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index b3a5f280..78c5b287 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -153,13 +153,13 @@ namespace EventContent { class TypedBase : public Base { public: + explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } protected: - explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} using Base::Base; }; -- cgit v1.2.3 From 110190d48a80a471e6d10d048602390b35e7ed07 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 18:24:39 +0200 Subject: Re-apply the previous commit and actually fix the breakage Ok, it was stupid to delete #include in 004ebf8d and then to expect that Qt macros would still work, given that I don't use QObject. In my defense I can only say that with Qt 6 it still compiled. --- lib/events/eventcontent.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 78c5b287..1d81bd72 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -10,6 +10,7 @@ #include #include #include +#include class QFileInfo; @@ -153,13 +154,13 @@ namespace EventContent { class TypedBase : public Base { public: - explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } protected: + explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} using Base::Base; }; -- 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. --- CMakeLists.txt | 6 +--- lib/connection.cpp | 4 +-- lib/connection.h | 1 - lib/events/roommemberevent.cpp | 65 ++++++++++++++++++++-------------------- lib/events/roommemberevent.h | 26 ++++++++-------- lib/joinstate.h | 32 -------------------- lib/quotient_common.cpp | 45 ++++++++++++++++++++++++++++ lib/quotient_common.h | 67 +++++++++++++++++++++++++++++++++++++----- lib/room.cpp | 49 ++++++++++++++++-------------- lib/room.h | 9 +++++- lib/syncdata.h | 2 +- lib/user.cpp | 2 +- 12 files changed, 190 insertions(+), 118 deletions(-) delete mode 100644 lib/joinstate.h create mode 100644 lib/quotient_common.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index deb50aea..49105389 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,11 +122,7 @@ endif () # Set up source files list(APPEND lib_SRCS - # This .h is special in that it declares a Q_NAMESPACE but has no .cpp - # where staticMetaObject for that namespace would be defined; passing it - # to add_library (see below) puts it on the automoc radar, producing - # a compilation unit with the needed definition. - lib/quotient_common.h + lib/quotient_common.cpp lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp diff --git a/lib/connection.cpp b/lib/connection.cpp index e076957a..7dd04aaa 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -640,7 +640,7 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, } qWarning(MAIN) << "Room" << roomData.roomId << "has just been forgotten but /sync returned it in" - << toCString(roomData.joinState) + << roomData.joinState << "state - suspiciously fast turnaround"; } if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) { @@ -1356,7 +1356,7 @@ void Connection::Private::removeRoom(const QString& roomId) for (auto f : { false, true }) if (auto r = roomMap.take({ roomId, f })) { qCDebug(MAIN) << "Room" << r->objectName() << "in state" - << toCString(r->joinState()) << "will be deleted"; + << r->joinState() << "will be deleted"; emit r->beforeDestruction(r); r->deleteLater(); } diff --git a/lib/connection.h b/lib/connection.h index 0d22d01f..a7a071f3 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -6,7 +6,6 @@ #pragma once #include "ssosession.h" -#include "joinstate.h" #include "qt_connection_util.h" #include "quotient_common.h" diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 9634ca3a..8a6bddd8 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -7,27 +7,26 @@ #include "converters.h" #include "logging.h" -#include - -static const std::array membershipStrings = { - { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), - QStringLiteral("leave"), QStringLiteral("ban") } -}; +#include namespace Quotient { template <> -struct JsonConverter { - static MembershipType load(const QJsonValue& jv) +struct JsonConverter { + static Membership load(const QJsonValue& jv) { - const auto& membershipString = jv.toString(); - for (auto it = membershipStrings.begin(); it != membershipStrings.end(); - ++it) - if (membershipString == *it) - return MembershipType(it - membershipStrings.begin()); - - if (!membershipString.isEmpty()) - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; - return MembershipType::Undefined; + const auto& ms = jv.toString(); + if (ms.isEmpty()) + { + qCWarning(EVENTS) << "Empty member state:" << ms; + return Membership::Invalid; + } + const auto it = + std::find(MembershipStrings.begin(), MembershipStrings.end(), ms); + if (it != MembershipStrings.end()) + return Membership(1U << (it - MembershipStrings.begin())); + + qCWarning(EVENTS) << "Unknown Membership value: " << ms; + return Membership::Invalid; } }; } // namespace Quotient @@ -35,7 +34,7 @@ struct JsonConverter { using namespace Quotient; MemberEventContent::MemberEventContent(const QJsonObject& json) - : membership(fromJson(json["membership"_ls])) + : membership(fromJson(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) , displayName(fromJson>(json["displayname"_ls])) , avatarUrl(fromJson>(json["avatar_url"_ls])) @@ -48,10 +47,12 @@ MemberEventContent::MemberEventContent(const QJsonObject& json) void MemberEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); - Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__, - "The key 'membership' must be explicit in MemberEventContent"); - if (membership != MembershipType::Undefined) - o->insert(QStringLiteral("membership"), membershipStrings[membership]); + if (membership != Membership::Invalid) + o->insert( + QStringLiteral("membership"), + MembershipStrings[qCountTrailingZeroBits( + std::underlying_type_t(membership)) + + 1]); if (displayName) o->insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) @@ -67,37 +68,37 @@ bool RoomMemberEvent::changesMembership() const bool RoomMemberEvent::isInvite() const { - return membership() == MembershipType::Invite && changesMembership(); + return membership() == Membership::Invite && changesMembership(); } bool RoomMemberEvent::isRejectedInvite() const { - return membership() == MembershipType::Leave && prevContent() - && prevContent()->membership == MembershipType::Invite; + return membership() == Membership::Leave && prevContent() + && prevContent()->membership == Membership::Invite; } bool RoomMemberEvent::isJoin() const { - return membership() == MembershipType::Join && changesMembership(); + return membership() == Membership::Join && changesMembership(); } bool RoomMemberEvent::isLeave() const { - return membership() == MembershipType::Leave && prevContent() + return membership() == Membership::Leave && prevContent() && prevContent()->membership != membership() - && prevContent()->membership != MembershipType::Ban - && prevContent()->membership != MembershipType::Invite; + && prevContent()->membership != Membership::Ban + && prevContent()->membership != Membership::Invite; } bool RoomMemberEvent::isBan() const { - return membership() == MembershipType::Ban && changesMembership(); + return membership() == Membership::Ban && changesMembership(); } bool RoomMemberEvent::isUnban() const { - return membership() == MembershipType::Leave && prevContent() - && prevContent()->membership == MembershipType::Ban; + return membership() == Membership::Leave && prevContent() + && prevContent()->membership == Membership::Ban; } bool RoomMemberEvent::isRename() const diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index f2fbe689..f3047159 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -7,23 +7,21 @@ #include "eventcontent.h" #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { class MemberEventContent : public EventContent::Base { public: - enum MembershipType : unsigned char { - Invite = 0, - Join, - Knock, - Leave, - Ban, - Undefined - }; + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {} + explicit MemberEventContent(Membership ms = Membership::Join) + : membership(ms) + {} explicit MemberEventContent(const QJsonObject& json); - MembershipType membership; + Membership membership; + /// (Only for invites) Whether the invite is to a direct chat bool isDirect = false; Omittable displayName; Omittable avatarUrl; @@ -33,15 +31,15 @@ protected: void fillJson(QJsonObject* o) const override; }; -using MembershipType = MemberEventContent::MembershipType; +using MembershipType [[deprecated("Use Membership instead")]] = Membership; class RoomMemberEvent : public StateEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) - using MembershipType = MemberEventContent::MembershipType; - Q_ENUM(MembershipType) + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} @@ -65,7 +63,7 @@ public: : StateEvent(type, fullJson) {} - MembershipType membership() const { return content().membership; } + Membership membership() const { return content().membership; } QString userId() const { return stateKey(); } bool isDirect() const { return content().isDirect; } Omittable newDisplayName() const { return content().displayName; } diff --git a/lib/joinstate.h b/lib/joinstate.h deleted file mode 100644 index 805ce73a..00000000 --- a/lib/joinstate.h +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2016 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -#include - -namespace Quotient { -enum class JoinState : unsigned int { - Join = 0x1, - Invite = 0x2, - Leave = 0x4, -}; - -Q_DECLARE_FLAGS(JoinStates, JoinState) - -// We cannot use Q_ENUM outside of a Q_OBJECT and besides, we want -// to use strings that match respective JSON keys. -static const std::array JoinStateStrings { { "join", "invite", - "leave" } }; - -inline const char* toCString(JoinState js) -{ - size_t state = size_t(js), index = 0; - while (state >>= 1u) - ++index; - return JoinStateStrings[index]; -} -} // namespace Quotient -Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) diff --git a/lib/quotient_common.cpp b/lib/quotient_common.cpp new file mode 100644 index 00000000..805070d1 --- /dev/null +++ b/lib/quotient_common.cpp @@ -0,0 +1,45 @@ +#include "quotient_common.h" + +#include + +using namespace Quotient; + +template +inline QDebug suppressScopeAndDump(QDebug dbg, Enum e) +{ + // Suppress "Quotient::" prefix + QDebugStateSaver _dss(dbg); + dbg.setVerbosity(QDebug::MinimumVerbosity); + return qt_QMetaEnum_debugOperator(dbg, std::underlying_type_t(e), + qt_getEnumMetaObject(e), + qt_getEnumName(e)); +} + +template +inline QDebug suppressScopeAndDump(QDebug dbg, const QFlags& f) +{ + // Suppress "Quotient::" prefix + QDebugStateSaver _dss(dbg); + dbg.setVerbosity(QDebug::MinimumVerbosity); + return qt_QMetaEnum_flagDebugOperator_helper(dbg, f); +} + +QDebug operator<<(QDebug dbg, Membership m) +{ + return suppressScopeAndDump(dbg, m); +} + +QDebug operator<<(QDebug dbg, MembershipMask mm) +{ + return suppressScopeAndDump(dbg, mm) << ")"; +} + +QDebug operator<<(QDebug dbg, JoinState js) +{ + return suppressScopeAndDump(dbg, js); +} + +QDebug operator<<(QDebug dbg, JoinStates jss) +{ + return suppressScopeAndDump(dbg, jss) << ")"; +} diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 22fdbe94..789128cf 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -8,15 +8,62 @@ namespace Quotient { Q_NAMESPACE -/** Enumeration with flags defining the network job running policy - * So far only background/foreground flags are available. - * - * \sa Connection::callApi, Connection::run - */ +// TODO: code like this should be generated from the CS API definition + +//! \brief Membership states +//! +//! These are used for member events. The names here are case-insensitively +//! equal to state names used on the wire. +//! \sa MemberEventContent, RoomMemberEvent +enum class Membership : unsigned int { + // Specific power-of-2 values (1,2,4,...) are important here as syncdata.cpp + // depends on that, as well as Join being the first in line + Invalid = 0x0, + Join = 0x1, + Leave = 0x2, + Invite = 0x4, + Knock = 0x8, + Ban = 0x10, + Undefined = Invalid +}; +Q_DECLARE_FLAGS(MembershipMask, Membership) +Q_FLAG_NS(MembershipMask) + +constexpr inline std::array MembershipStrings = { + // The order MUST be the same as the order in the original enum + "join", "leave", "invite", "knock", "ban" +}; + +//! \brief Local user join-state names +//! +//! This represents a subset of Membership values that may arrive as the local +//! user's state grouping for the sync response. +//! \sa SyncData +enum class JoinState : std::underlying_type_t { + Invalid = std::underlying_type_t(Membership::Invalid), + Join = std::underlying_type_t(Membership::Join), + Leave = std::underlying_type_t(Membership::Leave), + Invite = std::underlying_type_t(Membership::Invite), + Knock = std::underlying_type_t(Membership::Knock), +}; +Q_DECLARE_FLAGS(JoinStates, JoinState) +Q_FLAG_NS(JoinStates) + +constexpr inline std::array JoinStateStrings { + MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], + MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ +}; + +//! \brief Network job running policy flags +//! +//! So far only background/foreground flags are available. +//! \sa Connection::callApi, Connection::run enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; Q_ENUM_NS(RunningPolicy) +//! \brief The result of URI resolution using UriResolver +//! \sa UriResolver enum UriResolveResult : short { StillResolving = -1, UriResolved = 0, @@ -28,5 +75,11 @@ enum UriResolveResult : short { Q_ENUM_NS(UriResolveResult) } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) + +class QDebug; +QDebug operator<<(QDebug dbg, Quotient::Membership m); +QDebug operator<<(QDebug dbg, Quotient::MembershipMask m); +QDebug operator<<(QDebug dbg, Quotient::JoinState js); +QDebug operator<<(QDebug dbg, Quotient::JoinStates js); 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; diff --git a/lib/room.h b/lib/room.h index d71bff9c..cdbfe58f 100644 --- a/lib/room.h +++ b/lib/room.h @@ -11,7 +11,7 @@ #include "connection.h" #include "eventitem.h" -#include "joinstate.h" +#include "quotient_common.h" #include "csapi/message_pagination.h" @@ -245,6 +245,8 @@ public: /** * \brief Check the join state of a given user in this room * + * \deprecated Use memberState and check against a mask + * * \note Banned and invited users are not tracked separately for now (Leave * will be returned for them). * @@ -252,6 +254,11 @@ public: */ Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; + //! \brief Check the join state of a given user in this room + //! + //! \return the given user's state with respect to the room + Q_INVOKABLE Quotient::Membership memberState(User* user) const; + //! \brief Get a display name (without disambiguation) for the given member //! //! \sa safeMemberName, htmlSafeMemberName diff --git a/lib/syncdata.h b/lib/syncdata.h index e69bac17..0153bfd6 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -3,7 +3,7 @@ #pragma once -#include "joinstate.h" +#include "quotient_common.h" #include "events/stateevent.h" diff --git a/lib/user.cpp b/lib/user.cpp index 6cc9e161..c97e33a4 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -123,7 +123,7 @@ void User::rename(const QString& newName, const Room* r) } // #481: take the current state and update it with the new name auto evtC = r->getCurrentState(id())->content(); - Q_ASSERT_X(evtC.membership == MembershipType::Join, __FUNCTION__, + Q_ASSERT_X(evtC.membership == Membership::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); evtC.displayName = sanitized(newName); r->setState(id(), move(evtC)); -- cgit v1.2.3 From e05402045261a404ad5d8add63b82672d3d9aebb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 17 Jul 2021 13:47:19 +0200 Subject: Fix building with Qt 5.12 --- lib/quotient_common.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/quotient_common.cpp b/lib/quotient_common.cpp index 805070d1..5d7a3027 100644 --- a/lib/quotient_common.cpp +++ b/lib/quotient_common.cpp @@ -9,7 +9,7 @@ inline QDebug suppressScopeAndDump(QDebug dbg, Enum e) { // Suppress "Quotient::" prefix QDebugStateSaver _dss(dbg); - dbg.setVerbosity(QDebug::MinimumVerbosity); + dbg.setVerbosity(0 /* QDebug::MinimumVerbosity since Qt 5.13 */); return qt_QMetaEnum_debugOperator(dbg, std::underlying_type_t(e), qt_getEnumMetaObject(e), qt_getEnumName(e)); @@ -20,7 +20,7 @@ inline QDebug suppressScopeAndDump(QDebug dbg, const QFlags& f) { // Suppress "Quotient::" prefix QDebugStateSaver _dss(dbg); - dbg.setVerbosity(QDebug::MinimumVerbosity); + dbg.setVerbosity(0 /* QDebug::MinimumVerbosity since Qt 5.13 */); return qt_QMetaEnum_flagDebugOperator_helper(dbg, f); } -- cgit v1.2.3 From 3c28d13c1a61999e7c3141f3ca08b5b734bd160c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 19:00:07 +0200 Subject: Introduce to_array() to fix building on macOS A previous incarnation, make_array, existed in basejob.cpp before. The new direction taken by C++20 is to either deduce the array (but the used Apple standard library doesn't have deduction guides yet) or to use to_array() that converts a C array to std::array. This latter option is taken here, with to_array() defined in quotient_common.h until we move over to C++20. --- lib/jobs/basejob.cpp | 13 +++---------- lib/quotient_common.h | 27 ++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 9a7b9b5e..400a9243 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -5,6 +5,7 @@ #include "basejob.h" #include "connectiondata.h" +#include "quotient_common.h" #include #include @@ -15,8 +16,6 @@ #include #include -#include - using namespace Quotient; using std::chrono::seconds, std::chrono::milliseconds; using namespace std::chrono_literals; @@ -63,12 +62,6 @@ QDebug BaseJob::Status::dumpToLog(QDebug dbg) const return dbg << ": " << message; } -template -constexpr auto make_array(Ts&&... items) -{ - return std::array, sizeof...(Ts)>({items...}); -} - class BaseJob::Private { public: struct JobTimeoutConfig { @@ -163,8 +156,8 @@ public: { // FIXME: use std::array {} when Apple stdlib gets deduction guides for it static const auto verbs = - make_array(QStringLiteral("GET"), QStringLiteral("PUT"), - QStringLiteral("POST"), QStringLiteral("DELETE")); + to_array({ QStringLiteral("GET"), QStringLiteral("PUT"), + QStringLiteral("POST"), QStringLiteral("DELETE") }); const auto verbWord = verbs.at(size_t(verb)); return verbWord % ' ' % (reply ? reply->url().toString(QUrl::RemoveQuery) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 789128cf..037d5ded 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -8,6 +8,24 @@ namespace Quotient { Q_NAMESPACE +namespace impl { + template + constexpr std::array, N> + to_array_impl(T (&&a)[N], std::index_sequence) + { + return { {std::move(a[I])...} }; + } +} +// std::array {} needs explicit template parameters on macOS because +// Apple stdlib doesn't have deduction guides for std::array; to alleviate that, +// to_array() is borrowed from C++20 (thanks to cppreference for the possible +// implementation: https://en.cppreference.com/w/cpp/container/array/to_array) +template +constexpr auto to_array(T (&& items)[N]) +{ + return impl::to_array_impl(std::move(items), std::make_index_sequence{}); +} + // TODO: code like this should be generated from the CS API definition //! \brief Membership states @@ -29,10 +47,9 @@ enum class Membership : unsigned int { Q_DECLARE_FLAGS(MembershipMask, Membership) Q_FLAG_NS(MembershipMask) -constexpr inline std::array MembershipStrings = { +constexpr inline auto MembershipStrings = to_array( // The order MUST be the same as the order in the original enum - "join", "leave", "invite", "knock", "ban" -}; + { "join", "leave", "invite", "knock", "ban" }); //! \brief Local user join-state names //! @@ -49,10 +66,10 @@ enum class JoinState : std::underlying_type_t { Q_DECLARE_FLAGS(JoinStates, JoinState) Q_FLAG_NS(JoinStates) -constexpr inline std::array JoinStateStrings { +constexpr inline auto JoinStateStrings = to_array({ MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ -}; +}); //! \brief Network job running policy flags //! -- cgit v1.2.3 From e4a08bc431be9a2b680a4cd70f2ceda07c99b7bf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 20:10:35 +0200 Subject: Fix bit rot in comments --- lib/events/eventcontent.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 1d81bd72..40ec3a49 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -49,13 +49,14 @@ namespace EventContent { // but specific aggregation structure is altered. See doc comments to // each type for the list of available attributes. - // A quick classes inheritance structure follows: + // A quick classes inheritance structure follows (the definitions are + // spread across eventcontent.h and roommessageevent.h): // FileInfo - // FileContent : UrlBasedContent - // AudioContent : UrlBasedContent + // FileContent : UrlWithThumbnailContent + // AudioContent : PlayableContent> // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlBasedContent - // VideoContent : UrlBasedContent + // ImageContent : UrlWithThumbnailContent + // VideoContent : PlayableContent> /** * A base/mixin class for structures representing an "info" object for -- cgit v1.2.3 From 2187f26ebdc9edf7b3cbfa1d208c03c4384f4135 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Jul 2021 20:56:33 +0200 Subject: Add a missing #include Without this, it compiles on Linux but on macOS and Windows. --- lib/quotient_common.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 037d5ded..d225ad63 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -5,6 +5,8 @@ #include +#include + namespace Quotient { Q_NAMESPACE -- cgit v1.2.3 From f340d73ae5dac0d0cfee732aabbd5222c7be16dd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 24 Jul 2021 19:19:04 +0200 Subject: Make operator ""_ls constexpr --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index cb0ff44a..7f09de28 100644 --- a/lib/util.h +++ b/lib/util.h @@ -196,7 +196,7 @@ inline auto wrap_in_function(FnT&& f) return typename function_traits::function_type(std::forward(f)); } -inline auto operator"" _ls(const char* s, std::size_t size) +inline constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); } -- 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 +++++++--------- lib/syncdata.cpp | 12 ++++++------ lib/syncdata.h | 6 +++--- lib/util.h | 49 ++++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 58 insertions(+), 25 deletions(-) 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); diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 232f3694..d3c270b5 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -90,13 +90,13 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, } const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count"_ls).toInt(); - notificationCount = unreadJson.value("notification_count"_ls).toInt(); - if (highlightCount > 0 || notificationCount > 0) + fromJson(unreadJson.value(UnreadCountKey), unreadCount); + fromJson(unreadJson.value("highlight_count"_ls), highlightCount); + fromJson(unreadJson.value("notification_count"_ls), notificationCount); + if (highlightCount.has_value() || notificationCount.has_value()) qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << highlightCount - << "and notifications:" << notificationCount; + << "has highlights:" << *highlightCount + << "and notifications:" << *notificationCount; } SyncData::SyncData(const QString& cacheFileName) diff --git a/lib/syncdata.h b/lib/syncdata.h index 0153bfd6..b0e31726 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -48,9 +48,9 @@ public: bool timelineLimited; QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; + Omittable unreadCount; + Omittable highlightCount; + Omittable notificationCount; SyncRoomData(const QString& roomId, JoinState joinState_, const QJsonObject& room_); diff --git a/lib/util.h b/lib/util.h index 7f09de28..78fc9ab7 100644 --- a/lib/util.h +++ b/lib/util.h @@ -30,6 +30,13 @@ struct HashQ { template using UnorderedMap = std::unordered_map>; +namespace _impl { + template + constexpr inline auto IsOmittableValue = false; + template + constexpr inline auto IsOmittable = IsOmittableValue>; +} + constexpr auto none = std::nullopt; /** `std::optional` with tweaks @@ -107,18 +114,18 @@ public: return !this->has_value(); } - /// Merge the value from another Omittable - /// \return true if \p other is not omitted and the value of - /// the current Omittable was different (or omitted); - /// in other words, if the current Omittable has changed; - /// false otherwise + //! Merge the value from another Omittable + //! \return true if \p other is not omitted and the value of + //! the current Omittable was different (or omitted), + //! in other words, if the current Omittable has changed; + //! false otherwise template auto merge(const Omittable& other) - -> std::enable_if_t::value, bool> + -> std::enable_if_t, bool> { if (!other || (this->has_value() && **this == *other)) return false; - *this = other; + emplace(*other); return true; } @@ -132,6 +139,34 @@ public: value_type& operator*() && { return base_type::operator*(); } }; +namespace _impl { + template + constexpr inline auto IsOmittableValue> = true; +} + +template +inline auto merge(Omittable& lhs, T2&& rhs) +{ + return lhs.merge(std::forward(rhs)); +} + +//! \brief Merge the value from an Omittable +//! This is an adaptation of Omittable::merge() to the case when the value +//! on the left hand side is not an Omittable. +//! \return true if \p rhs is not omitted and the \p lhs value was different, +//! in other words, if \p lhs has changed; +//! false otherwise +template +inline auto merge(T1& lhs, const Omittable& rhs) + -> std::enable_if_t + && std::is_convertible_v, bool> +{ + if (!rhs || lhs == *rhs) + return false; + lhs = *rhs; + return true; +} + namespace _impl { template struct fn_traits {}; -- 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(+) 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 000b57306afe450c21df3aa95313567614c34516 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 28 Jul 2021 15:08:05 +0200 Subject: .clang-format: revert BraceWrapping.AfterControlStatement It triggers a bug in libformat that prevents AllowShortFunctionsOnASingleLine to do its job: https://bugs.llvm.org/show_bug.cgi?id=47936 --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 72b67488..3eafee44 100644 --- a/.clang-format +++ b/.clang-format @@ -44,7 +44,7 @@ AlwaysBreakTemplateDeclarations: Yes BraceWrapping: AfterCaseLabel: false AfterClass: false - AfterControlStatement: MultiLine + AfterControlStatement: Never # Switch to MultiLine, once https://bugs.llvm.org/show_bug.cgi?id=47936 is fixed AfterEnum: false AfterFunction: true AfterNamespace: false -- 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 ++++++++----- lib/room.h | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) 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; } diff --git a/lib/room.h b/lib/room.h index cdbfe58f..fa7b6e6d 100644 --- a/lib/room.h +++ b/lib/room.h @@ -364,7 +364,7 @@ public: rev_iter_t readMarker(const User* user) const; rev_iter_t readMarker() const; QString readMarkerEventId() const; - QList usersAtEventId(const QString& eventId); + QSet usersAtEventId(const QString& eventId); /** * \brief Mark the event with uptoEventId as read * -- cgit v1.2.3 From ed1b034089dfc1948acaa80a5200cb0df28d0c1c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 26 Jul 2021 22:36:44 +0200 Subject: SyncData: minor update to the cache version The minor component is now updated in .cpp, not in .h. --- lib/syncdata.cpp | 7 ++++++- lib/syncdata.h | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index d3c270b5..3e0eff17 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -103,7 +103,7 @@ SyncData::SyncData(const QString& cacheFileName) { QFileInfo cacheFileInfo { cacheFileName }; auto json = loadJson(cacheFileName); - auto requiredVersion = std::get<0>(cacheVersion()); + auto requiredVersion = MajorCacheVersion; auto actualVersion = json.value("cache_version"_ls).toObject().value("major"_ls).toInt(); if (actualVersion == requiredVersion) @@ -128,6 +128,11 @@ Events&& SyncData::takeAccountData() { return std::move(accountData); } Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } +std::pair SyncData::cacheVersion() +{ + return { MajorCacheVersion, 1 }; +} + QJsonObject SyncData::loadJson(const QString& fileName) { QFile roomFile { fileName }; diff --git a/lib/syncdata.h b/lib/syncdata.h index b0e31726..b869a541 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -87,7 +87,8 @@ public: QStringList unresolvedRooms() const { return unresolvedRoomIds; } - static std::pair cacheVersion() { return { 11, 0 }; } + static constexpr int MajorCacheVersion = 11; + static std::pair cacheVersion(); static QString fileNameForRoom(QString roomId); private: -- 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(-) 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 70035f204345b09305fbb487208708e69bd79a53 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 29 Jul 2021 21:59:03 +0200 Subject: Formatting --- lib/room.h | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/room.h b/lib/room.h index cdbfe58f..a1cb6261 100644 --- a/lib/room.h +++ b/lib/room.h @@ -181,12 +181,10 @@ public: QString name() const; /// Room aliases defined on the current user's server /// \sa remoteAliases, setLocalAliases - [[deprecated("Use aliases()")]] - QStringList localAliases() const; + [[deprecated("Use aliases()")]] QStringList localAliases() const; /// Room aliases defined on other servers /// \sa localAliases - [[deprecated("Use aliases()")]] - QStringList remoteAliases() const; + [[deprecated("Use aliases()")]] QStringList remoteAliases() const; QString canonicalAlias() const; QStringList altAliases() const; QStringList aliases() const; @@ -271,7 +269,8 @@ public: */ Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; /*! - * \brief Get a disambiguated name for a user with this id in the room context + * \brief Get a disambiguated name for a user with this id in the room + * context * * \deprecated use safeMemberName() instead */ @@ -339,9 +338,13 @@ public: const char* relType) const; const RoomCreateEvent* creation() const - { return getCurrentState(); } + { + return getCurrentState(); + } const RoomTombstoneEvent* tombstone() const - { return getCurrentState(); } + { + return getCurrentState(); + } bool displayed() const; /// Mark the room as currently displayed to the user -- cgit v1.2.3 From 42f20f827565a2bdd9e1373b7ee2c408991b9d44 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 30 Jul 2021 08:07:59 +0200 Subject: .clang-format: add DEFINE_EVENT_TYPEID to StatementMacros --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index 3eafee44..6e13223e 100644 --- a/.clang-format +++ b/.clang-format @@ -133,6 +133,7 @@ Standard: c++17 StatementMacros: - Q_UNUSED - QT_REQUIRE_VERSION + - DEFINE_EVENT_TYPEID TabWidth: 4 #UseCRLF: false #UseTab: Never -- 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/events/simplestateevents.h | 6 ++++-- lib/room.cpp | 30 +++++------------------------- lib/room.h | 14 ++------------ 3 files changed, 11 insertions(+), 39 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index d6261a8f..3bac54e6 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -56,8 +56,10 @@ namespace EventContent { DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) -class RoomAliasesEvent - : public StateEvent> { +class [[deprecated( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // +RoomAliasesEvent : public StateEvent> { public: DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) explicit RoomAliasesEvent(const QJsonObject& obj) 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); } diff --git a/lib/room.h b/lib/room.h index a1cb6261..0ab18ef7 100644 --- a/lib/room.h +++ b/lib/room.h @@ -93,7 +93,6 @@ class Room : public QObject { Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ safeMemberNames NOTIFY memberListChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) @@ -179,14 +178,9 @@ public: Room* successor(JoinStates statesFilter = JoinState::Invite | JoinState::Join) const; QString name() const; - /// Room aliases defined on the current user's server - /// \sa remoteAliases, setLocalAliases - [[deprecated("Use aliases()")]] QStringList localAliases() const; - /// Room aliases defined on other servers - /// \sa localAliases - [[deprecated("Use aliases()")]] QStringList remoteAliases() const; QString canonicalAlias() const; QStringList altAliases() const; + //! Get a list of both canonical and alternative aliases QStringList aliases() const; QString displayName() const; QString topic() const; @@ -202,8 +196,6 @@ public: QStringList memberNames() const; QStringList safeMemberNames() const; QStringList htmlSafeMemberNames() const; - [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] - int memberCount() const; int timelineSize() const; bool usesEncryption() const; RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); @@ -320,8 +312,6 @@ public: * arrived event; same as messageEvents().cend() */ Timeline::const_iterator syncEdge() const; - /// \deprecated Use historyEdge instead - rev_iter_t timelineEdge() const; Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const; Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const; Q_INVOKABLE bool @@ -387,7 +377,7 @@ public: * events (non-redacted message events from users other than local) * are counted. * - * In a case when readMarker() == timelineEdge() (the local read + * In a case when readMarker() == historyEdge() (the local read * marker is beyond the local timeline) only the bottom limit of * the unread messages number can be estimated (and even that may * be slightly off due to, e.g., redactions of events not loaded -- 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(-) 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 7ee1681d7640b7e7683f7bb40bf768704a48832c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 30 Jul 2021 08:22:13 +0200 Subject: Clean up after the previous commit RoomAliasesEvent is no more even registered (meaning that the library will load m.room.aliases as unknown state events); quotest code updated to use historyEdge() instead of timelineEdge(). --- lib/events/simplestateevents.h | 1 - quotest/quotest.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 3bac54e6..cf1bfbba 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -72,5 +72,4 @@ public: QString server() const { return stateKey(); } QStringList aliases() const { return content().value; } }; -REGISTER_EVENT_TYPE(RoomAliasesEvent) } // namespace Quotient diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 5646d54d..ec7d4dcb 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -557,7 +557,7 @@ bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest, // redacted at the next sync, or the nearest sync completes with // the unredacted event but the next one brings redaction. auto it = targetRoom->findInTimeline(evtIdToRedact); - if (it == targetRoom->timelineEdge()) + if (it == targetRoom->historyEdge()) return false; // Waiting for the next sync if ((*it)->isRedacted()) { -- 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 ++++++++++++++++++++++++++++++++++-------------------------- lib/room.h | 39 +++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 35 deletions(-) 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 diff --git a/lib/room.h b/lib/room.h index fa7b6e6d..4833bd27 100644 --- a/lib/room.h +++ b/lib/room.h @@ -71,6 +71,28 @@ public: bool failed() const { return status == Failed; } }; +//! \brief Data structure for a room member's read receipt +//! \sa Room::lastReadReceipt +class ReadReceipt { + Q_GADGET + Q_PROPERTY(QString eventId MEMBER eventId CONSTANT) + Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT) +public: + QString eventId; + QDateTime timestamp; + + bool operator==(const ReadReceipt& other) + { + return eventId == other.eventId && timestamp == other.timestamp; + } + bool operator!=(const ReadReceipt& other) { return !operator==(other); } +}; +inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) +{ + swap(lhs.eventId, rhs.eventId); + swap(lhs.timestamp, rhs.timestamp); +} + class Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) @@ -104,9 +126,11 @@ class Room : public QObject { setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) - + //! \deprecated since 0.7 Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) + Q_PROPERTY(QString lastFullyReadEventId READ lastFullyReadEventId WRITE + markMessagesAsRead NOTIFY fullyReadMarkerMoved) Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) @@ -361,9 +385,18 @@ public: void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker(const User* user) const; + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker() const; + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // QString readMarkerEventId() const; + ReadReceipt lastReadReceipt(const QString& userId) const; + QString lastFullyReadEventId() const; + rev_iter_t fullyReadMarker() const; QSet usersAtEventId(const QString& eventId); /** * \brief Mark the event with uptoEventId as read @@ -704,7 +737,10 @@ Q_SIGNALS: void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); void lastReadEventChanged(Quotient::User* user); + void fullyReadMarkerMoved(QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use fullyReadMarkerMoved void readMarkerMoved(QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use lastReadEventChanged void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, QString toEventId); void unreadMessagesChanged(Quotient::Room* room); @@ -779,4 +815,5 @@ private: }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::FileTransferInfo) +Q_DECLARE_METATYPE(Quotient::ReadReceipt) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes) -- 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 +++++++++ lib/room.h | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) 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()) { diff --git a/lib/room.h b/lib/room.h index 9ef553df..afe223c5 100644 --- a/lib/room.h +++ b/lib/room.h @@ -626,7 +626,12 @@ public Q_SLOTS: void downloadFile(const QString& eventId, const QUrl& localFilename = {}); void cancelFileTransfer(const QString& id); - /// Mark all messages in the room as read + //! \brief Set a given event as last read and post a read receipt on it + //! + //! Does nothing if the event is behind the current read receipt. + //! \sa lastReadReceipt, markMessagesAsRead, markAllMessagesAsRead + void setReadReceipt(const QString& atEventId); + //! Put the fully-read marker at the latest message in the room void markAllMessagesAsRead(); /// Switch the room's version (aka upgrade) -- cgit v1.2.3 From 6d558fa9c12b52fe3cc47cc143d0a758c4ea929a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 31 Jul 2021 08:30:44 +0200 Subject: Add/update doc-comments on the new/updated methods [skip ci] --- lib/room.h | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/lib/room.h b/lib/room.h index afe223c5..a8c3cac2 100644 --- a/lib/room.h +++ b/lib/room.h @@ -378,30 +378,85 @@ public: void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); + //! \brief Obtain a read receipt of any user + //! + //! \deprecated Use lastReadReceipt or fullyReadMarker instead + //! Historically, readMarker was returning a "converged" read marker + //! representing both the read receipt and the fully read marker, as + //! Quotient managed them together. Since 0.6.8, a single-argument call of + //! readMarker returns the last read receipt position (for any room member) + //! and a call without arguments returns the last _fully read_ position, + //! to provide access to both positions separately while maintaining API + //! stability guarantees. 0.7 has separate methods to return read receipts + //! and the fully read marker - use them instead. + //! \sa lastReadReceipt [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker(const User* user) const; + //! \brief Obtain the local user's fully-read marker + //! \deprecated Use fullyReadMarker instead + //! See the documentation for the single-argument overload + //! \sa fullyReadMarker [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker() const; + //! \brief Get the event id for the local user's fully-read marker + //! \deprecated Use lastFullyReadEventId instead + //! See the readMarker documentation [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // QString readMarkerEventId() const; + + //! \brief Get the latest read receipt from a user + //! + //! The user id must be valid. A read receipt with an empty event id + //! is returned if the user id is valid but there was no read receipt + //! from them. + //! \sa usersAtEventId ReadReceipt lastReadReceipt(const QString& userId) const; + + //! \brief Get the latest event id marked as fully read + //! + //! This can be either the event id pointed to by the actual latest + //! m.fully_read event, or the latest event id marked locally as fully read + //! if markMessagesAsRead or markAllMessagesAsRead has been called and + //! the homeserver didn't return an updated m.fully_read event yet. + //! \sa markMessagesAsRead, markAllMessagesAsRead, fullyReadMarker QString lastFullyReadEventId() const; + + //! \brief Get the iterator to the latest timeline item marked as fully read + //! + //! This method calls findInTimeline on the result of lastFullyReadEventId. + //! If the fully read marker turns out to be outside the timeline (because + //! the event marked as fully read is too far back in the history) the + //! returned value will be equal to historyEdge. + //! + //! Be sure to read the caveats on iterators returned by findInTimeline. + //! \sa lastFullyReadEventId, findInTimeline rev_iter_t fullyReadMarker() const; + + //! \brief Get users whose latest read receipts point to the event + //! + //! This method is for cases when you need to show users who have read + //! an event. Calling it on inexistent or empty event id will return + //! an empty set. + //! \sa lastReadReceipt QSet usersAtEventId(const QString& eventId); - /** - * \brief Mark the event with uptoEventId as read - * - * Finds in the timeline and marks as read the event with - * the specified id; also posts a read receipt to the server either - * for this message or, if it's from the local user, for - * the nearest non-local message before. uptoEventId must be non-empty. - */ - void markMessagesAsRead(QString uptoEventId); - /// Check whether there are unread messages in the room + //! + //! \brief Mark the event with uptoEventId as fully read + //! + //! Marks the event with the specified id as fully read locally and also + //! sends an update to m.fully_read account data to the server either + //! for this message or, if it's from the local user, for + //! the nearest non-local message before. uptoEventId must point to a known + //! event in the timeline; the method will do nothing if the event is behind + //! the current m.fully_read marker or is not loaded, to prevent + //! accidentally trying to move the marker back in the timeline. + //! \sa markAllMessagesAsRead, fullyReadMarker + Q_INVOKABLE void markMessagesAsRead(QString uptoEventId); + + //! Check whether there are unread messages in the room bool hasUnreadMessages() const; /** Get the number of unread messages in the room -- cgit v1.2.3 From 5c3f853a04a0c1a2b360391a9f27e7c0fd9f42bd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 31 Jul 2021 08:32:35 +0200 Subject: Room: Mark dependent Q_PROPERTYs as STORED false hasUnreadMessages is derived from unreadCount; isFavourite/isLowPriority effectively depend on tagNames. --- lib/room.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/room.h b/lib/room.h index 0ab18ef7..52ba2eab 100644 --- a/lib/room.h +++ b/lib/room.h @@ -107,7 +107,7 @@ class Room : public QObject { Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY - unreadMessagesChanged) + unreadMessagesChanged STORED false) Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) Q_PROPERTY(int highlightCount READ highlightCount NOTIFY highlightCountChanged RESET resetHighlightCount) @@ -116,8 +116,8 @@ class Room : public QObject { Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages STORED false) Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) - Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) - Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) + Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged STORED false) + Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged STORED false) Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) -- cgit v1.2.3 From ea1e849f617f62b3d209b2019e0daa3c6bed50f0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 2 Aug 2021 09:02:31 +0200 Subject: More doc-comments --- lib/events/roomevent.h | 4 ++++ lib/events/roommessageevent.h | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index fea509c0..3abd56c0 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -37,6 +37,10 @@ public: } QString roomId() const; QString senderId() const; + //! \brief Determine whether the event has been replaced + //! + //! \return true if this event has been overridden by another event + //! with `"rel_type": "m.replace"`; false otherwise bool isReplaced() const; QString replacedBy() const; bool isRedacted() const { return bool(_redactedBecause); } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 7bcda2ba..88d3b74c 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -62,9 +62,26 @@ public: _content.data()); } QMimeType mimeType() const; + //! \brief Determine whether the message has text content + //! + //! \return true, if the message type is one of m.text, m.notice, m.emote, + //! or the message type is unspecified (in which case plainBody() + //! can still be examined); false otherwise bool hasTextContent() const; + //! \brief Determine whether the message has a file/attachment + //! + //! \return true, if the message has a data structure corresponding to + //! a file (such as m.file or m.audio); false otherwise bool hasFileContent() const; + //! \brief Determine whether the message has a thumbnail + //! + //! \return true, if the message has a data structure corresponding to + //! a thumbnail (the message type may be one for visual content, + //! such as m.image, or generic binary content, i.e. m.file); + //! false otherwise bool hasThumbnail() const; + //! \brief Obtain id of an event replaced by the current one + //! \sa RoomEvent::isReplaced, RoomEvent::replacedBy QString replacedEvent() const; static QString rawMsgTypeForUrl(const QUrl& url); -- cgit v1.2.3 From 72643d2f90aa929ec5e44159f717057fdad56cbd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 2 Aug 2021 09:04:06 +0200 Subject: Fix lack of percent encoding in User::load() Users with slashes in their ids do it at their own peril of course but to encode the id in the URL is a good thing in any case. Too bad it's pretty invisible and has to be dealt with case by case, instead of GTAD magically sticking QUrl::toPercentEncoding() where appropriate in the generated code. --- lib/user.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/user.cpp b/lib/user.cpp index c97e33a4..04afed2b 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -65,7 +65,8 @@ User::~User() = default; void User::load() { - auto *profileJob = connection()->callApi(id()); + auto* profileJob = + connection()->callApi(QUrl::toPercentEncoding(id())); connect(profileJob, &BaseJob::result, this, [this, profileJob] { d->defaultName = profileJob->displayname(); d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); -- cgit v1.2.3 From 1f52b5a2da9bce4d25f4c897370e58c8b6d56ba1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 11:08:57 +0200 Subject: README.md: replace "PRs welcome" with a merge chance badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5aae543..05c629f2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) ![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/quotient-im/libQuotient.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quotient-im/libQuotient/context:cpp) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +[![merge-chance-badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fmerge-chance.info%2Fbadge%3Frepo%3Dquotient-im/libquotient)](https://merge-chance.info/target?repo=quotient-im/libquotient) The Quotient project aims to produce a Qt5-based SDK to develop applications for [Matrix](https://matrix.org). libQuotient is a library that enables client -- cgit v1.2.3 From f7cbefe5ad626ae1798a5d7bb7546e89ad336acd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 16:55:06 +0200 Subject: API files: reformat after .clang-format change See 000b5730. --- lib/csapi/admin.h | 5 +---- lib/csapi/content-repo.h | 35 +++++++---------------------------- lib/csapi/create_room.h | 5 +---- lib/csapi/device_management.h | 5 +---- lib/csapi/directory.h | 5 +---- lib/csapi/event_context.h | 20 ++++---------------- lib/csapi/filter.h | 10 ++-------- lib/csapi/joining.h | 10 ++-------- lib/csapi/keys.h | 5 +---- lib/csapi/knocking.h | 5 +---- lib/csapi/list_public_rooms.h | 20 ++++---------------- lib/csapi/login.h | 10 ++-------- lib/csapi/message_pagination.h | 20 ++++---------------- lib/csapi/notifications.h | 5 +---- lib/csapi/openid.h | 5 +---- lib/csapi/peeking_events.h | 15 +++------------ lib/csapi/presence.h | 10 ++-------- lib/csapi/profile.h | 10 ++-------- lib/csapi/pushrules.h | 10 ++-------- lib/csapi/redaction.h | 5 +---- lib/csapi/registration.h | 10 ++-------- lib/csapi/room_send.h | 5 +---- lib/csapi/room_state.h | 5 +---- lib/csapi/rooms.h | 12 ++---------- lib/csapi/users.h | 5 +---- lib/csapi/voip.h | 5 +---- lib/csapi/whoami.h | 10 ++-------- 27 files changed, 53 insertions(+), 214 deletions(-) diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index d4fe639b..570bf24a 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -74,10 +74,7 @@ public: // Result properties /// The Matrix user ID of the user. - QString userId() const - { - return loadFromJson("user_id"_ls); - } + QString userId() const { return loadFromJson("user_id"_ls); } /// Each key is an identifier for one of the user's devices. QHash devices() const diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index a41453b2..f3d7309a 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -72,10 +72,7 @@ public: // Result properties /// The content type of the file that was previously uploaded. - QString contentType() const - { - return reply()->rawHeader("Content-Type"); - } + QString contentType() const { return reply()->rawHeader("Content-Type"); } /// The name of the file that was previously uploaded, if set. QString contentDisposition() const @@ -84,10 +81,7 @@ public: } /// The content that was previously uploaded. - QIODevice* data() - { - return reply(); - } + QIODevice* data() { return reply(); } }; /*! \brief Download content from the content repository overriding the file name @@ -132,10 +126,7 @@ public: // Result properties /// The content type of the file that was previously uploaded. - QString contentType() const - { - return reply()->rawHeader("Content-Type"); - } + QString contentType() const { return reply()->rawHeader("Content-Type"); } /// The `fileName` requested or the name of the file that was previously /// uploaded, if set. @@ -145,10 +136,7 @@ public: } /// The content that was previously uploaded. - QIODevice* data() - { - return reply(); - } + QIODevice* data() { return reply(); } }; /*! \brief Download a thumbnail of content from the content repository @@ -202,16 +190,10 @@ public: // Result properties /// The content type of the thumbnail. - QString contentType() const - { - return reply()->rawHeader("Content-Type"); - } + QString contentType() const { return reply()->rawHeader("Content-Type"); } /// A thumbnail of the requested content. - QIODevice* data() - { - return reply(); - } + QIODevice* data() { return reply(); } }; /*! \brief Get information about a URL for a client @@ -257,10 +239,7 @@ public: /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image. /// Omitted if there is no image. - QString ogImage() const - { - return loadFromJson("og:image"_ls); - } + QString ogImage() const { return loadFromJson("og:image"_ls); } }; /*! \brief Get the configuration for the content repository. diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 8c6af7d4..81dfbffc 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -268,10 +268,7 @@ public: // Result properties /// The created room's ID. - QString roomId() const - { - return loadFromJson("room_id"_ls); - } + QString roomId() const { return loadFromJson("room_id"_ls); } }; template <> diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index e2acea18..7fb69873 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -59,10 +59,7 @@ public: // Result properties /// Device information - Device device() const - { - return fromJson(jsonData()); - } + Device device() const { return fromJson(jsonData()); } }; /*! \brief Update a device diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 00215cae..93a31595 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -51,10 +51,7 @@ public: // Result properties /// The room ID for this room alias. - QString roomId() const - { - return loadFromJson("room_id"_ls); - } + QString roomId() const { return loadFromJson("room_id"_ls); } /// A list of servers that are aware of this room alias. QStringList servers() const diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index 6a49769f..4e50edf3 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -58,16 +58,10 @@ public: // Result properties /// A token that can be used to paginate backwards with. - QString begin() const - { - return loadFromJson("start"_ls); - } + QString begin() const { return loadFromJson("start"_ls); } /// A token that can be used to paginate forwards with. - QString end() const - { - return loadFromJson("end"_ls); - } + QString end() const { return loadFromJson("end"_ls); } /// A list of room events that happened just before the /// requested event, in reverse-chronological order. @@ -77,10 +71,7 @@ public: } /// Details of the requested event. - RoomEventPtr event() - { - return takeFromJson("event"_ls); - } + RoomEventPtr event() { return takeFromJson("event"_ls); } /// A list of room events that happened just after the /// requested event, in chronological order. @@ -90,10 +81,7 @@ public: } /// The state of the room at the last event returned. - StateEvents state() - { - return takeFromJson("state"_ls); - } + StateEvents state() { return takeFromJson("state"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index 7e9e14ee..01bec36b 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -35,10 +35,7 @@ public: /// with a `{` as this character is used to determine /// if the filter provided is inline JSON or a previously /// declared filter by homeservers on some APIs. - QString filterId() const - { - return loadFromJson("filter_id"_ls); - } + QString filterId() const { return loadFromJson("filter_id"_ls); } }; /*! \brief Download a filter @@ -67,10 +64,7 @@ public: // Result properties /// The filter definition. - Filter filter() const - { - return fromJson(jsonData()); - } + Filter filter() const { return fromJson(jsonData()); } }; } // namespace Quotient diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 6dcd1351..d0199b11 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -49,10 +49,7 @@ public: // Result properties /// The joined room ID. - QString roomId() const - { - return loadFromJson("room_id"_ls); - } + QString roomId() const { return loadFromJson("room_id"_ls); } }; /*! \brief Start the requesting user participating in a particular room. @@ -98,10 +95,7 @@ public: // Result properties /// The joined room ID. - QString roomId() const - { - return loadFromJson("room_id"_ls); - } + QString roomId() const { return loadFromJson("room_id"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 53ba6495..7db09e8d 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -267,10 +267,7 @@ public: /// The Matrix User IDs of all users who may have left all /// the end-to-end encrypted rooms they previously shared /// with the user. - QStringList left() const - { - return loadFromJson("left"_ls); - } + QStringList left() const { return loadFromJson("left"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h index 607b55a9..1108cb64 100644 --- a/lib/csapi/knocking.h +++ b/lib/csapi/knocking.h @@ -49,10 +49,7 @@ public: // Result properties /// The knocked room ID. - QString roomId() const - { - return loadFromJson("room_id"_ls); - } + QString roomId() const { return loadFromJson("room_id"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 1c73c0af..963c8b56 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -111,18 +111,12 @@ public: /// A pagination token for the response. The absence of this token /// means there are no more results to fetch and the client should /// stop paginating. - QString nextBatch() const - { - return loadFromJson("next_batch"_ls); - } + QString nextBatch() const { return loadFromJson("next_batch"_ls); } /// A pagination token that allows fetching previous results. The /// absence of this token means there are no results before this /// batch, i.e. this is the first batch. - QString prevBatch() const - { - return loadFromJson("prev_batch"_ls); - } + QString prevBatch() const { return loadFromJson("prev_batch"_ls); } /// An estimate on the total number of public rooms, if the /// server has an estimate. @@ -196,18 +190,12 @@ public: /// A pagination token for the response. The absence of this token /// means there are no more results to fetch and the client should /// stop paginating. - QString nextBatch() const - { - return loadFromJson("next_batch"_ls); - } + QString nextBatch() const { return loadFromJson("next_batch"_ls); } /// A pagination token that allows fetching previous results. The /// absence of this token means there are no results before this /// batch, i.e. this is the first batch. - QString prevBatch() const - { - return loadFromJson("prev_batch"_ls); - } + QString prevBatch() const { return loadFromJson("prev_batch"_ls); } /// An estimate on the total number of public rooms, if the /// server has an estimate. diff --git a/lib/csapi/login.h b/lib/csapi/login.h index ce783af2..b35db1eb 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -121,10 +121,7 @@ public: // Result properties /// The fully-qualified Matrix ID for the account. - QString userId() const - { - return loadFromJson("user_id"_ls); - } + QString userId() const { return loadFromJson("user_id"_ls); } /// An access token for the account. /// This access token can then be used to authorize other requests. @@ -146,10 +143,7 @@ public: /// ID of the logged-in device. Will be the same as the /// corresponding parameter in the request, if one was specified. - QString deviceId() const - { - return loadFromJson("device_id"_ls); - } + QString deviceId() const { return loadFromJson("device_id"_ls); } /// Optional client configuration provided by the server. If present, /// clients SHOULD use the provided object to reconfigure themselves, diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 020ef543..363e4d99 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -66,26 +66,17 @@ public: /// The token the pagination starts from. If `dir=b` this will be /// the token supplied in `from`. - QString begin() const - { - return loadFromJson("start"_ls); - } + QString begin() const { return loadFromJson("start"_ls); } /// The token the pagination ends at. If `dir=b` this token should /// be used again to request even earlier events. - QString end() const - { - return loadFromJson("end"_ls); - } + QString end() const { return loadFromJson("end"_ls); } /// A list of room events. The order depends on the `dir` parameter. /// For `dir=b` events will be in reverse-chronological order, /// for `dir=f` in chronological order, so that events start /// at the `from` point. - RoomEvents chunk() - { - return takeFromJson("chunk"_ls); - } + RoomEvents chunk() { return takeFromJson("chunk"_ls); } /// A list of state events relevant to showing the `chunk`. For example, if /// `lazy_load_members` is enabled in the filter then this may contain @@ -95,10 +86,7 @@ public: /// may remove membership events which would have already been /// sent to the client in prior calls to this endpoint, assuming /// the membership of those members has not changed. - StateEvents state() - { - return takeFromJson("state"_ls); - } + StateEvents state() { return takeFromJson("state"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 0cc165ce..0c38fe6b 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -71,10 +71,7 @@ public: /// The token to supply in the `from` param of the next /// `/notifications` request in order to request more /// events. If this is absent, there are no more results. - QString nextToken() const - { - return loadFromJson("next_token"_ls); - } + QString nextToken() const { return loadFromJson("next_token"_ls); } /// The list of events that triggered notifications. std::vector notifications() diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 88218c20..0be39c8c 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -43,10 +43,7 @@ public: /// Specification](http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse) /// with the only difference being the lack of an `id_token`. Instead, /// the Matrix homeserver's name is provided. - OpenidToken tokenData() const - { - return fromJson(jsonData()); - } + OpenidToken tokenData() const { return fromJson(jsonData()); } }; } // namespace Quotient diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 1eee880f..885ff340 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -53,23 +53,14 @@ public: /// A token which correlates to the first value in `chunk`. This /// is usually the same token supplied to `from=`. - QString begin() const - { - return loadFromJson("start"_ls); - } + QString begin() const { return loadFromJson("start"_ls); } /// A token which correlates to the last value in `chunk`. This /// token should be used in the next request to `/events`. - QString end() const - { - return loadFromJson("end"_ls); - } + QString end() const { return loadFromJson("end"_ls); } /// An array of events. - RoomEvents chunk() - { - return takeFromJson("chunk"_ls); - } + RoomEvents chunk() { return takeFromJson("chunk"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index c817ad9f..4ab50e25 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -55,10 +55,7 @@ public: // Result properties /// This user's presence. - QString presence() const - { - return loadFromJson("presence"_ls); - } + QString presence() const { return loadFromJson("presence"_ls); } /// The length of time in milliseconds since an action was performed /// by this user. @@ -68,10 +65,7 @@ public: } /// The state message for this user if one was set. - QString statusMsg() const - { - return loadFromJson("status_msg"_ls); - } + QString statusMsg() const { return loadFromJson("status_msg"_ls); } /// Whether the user is currently active Omittable currentlyActive() const diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 3cda34f8..8bbe4f8c 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -101,10 +101,7 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const - { - return loadFromJson("avatar_url"_ls); - } + QString avatarUrl() const { return loadFromJson("avatar_url"_ls); } }; /*! \brief Get this user's profile information. @@ -133,10 +130,7 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const - { - return loadFromJson("avatar_url"_ls); - } + QString avatarUrl() const { return loadFromJson("avatar_url"_ls); } /// The user's display name if they have set one, otherwise not present. QString displayname() const diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index 90d2ce79..a5eb48f0 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -72,10 +72,7 @@ public: /// The specific push rule. This will also include keys specific to the /// rule itself such as the rule's `actions` and `conditions` if set. - PushRule pushRule() const - { - return fromJson(jsonData()); - } + PushRule pushRule() const { return fromJson(jsonData()); } }; /*! \brief Delete a push rule. @@ -191,10 +188,7 @@ public: // Result properties /// Whether the push rule is enabled or not. - bool enabled() const - { - return loadFromJson("enabled"_ls); - } + bool enabled() const { return loadFromJson("enabled"_ls); } }; /*! \brief Enable or disable a push rule. diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index f12e6b71..f0db9f9f 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -46,10 +46,7 @@ public: // Result properties /// A unique identifier for the event. - QString eventId() const - { - return loadFromJson("event_id"_ls); - } + QString eventId() const { return loadFromJson("event_id"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 0ad8b101..c1614f20 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -108,10 +108,7 @@ public: /// /// Any user ID returned by this API must conform to the grammar given in /// the [Matrix specification](/appendices/#user-identifiers). - QString userId() const - { - return loadFromJson("user_id"_ls); - } + QString userId() const { return loadFromJson("user_id"_ls); } /// An access token for the account. /// This access token can then be used to authorize other requests. @@ -135,10 +132,7 @@ public: /// ID of the registered device. Will be the same as the /// corresponding parameter in the request, if one was specified. /// Required if the `inhibit_login` option is false. - QString deviceId() const - { - return loadFromJson("device_id"_ls); - } + QString deviceId() const { return loadFromJson("device_id"_ls); } }; /*! \brief Begins the validation process for an email to be used during diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index a9e7ca13..96f5beca 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -49,10 +49,7 @@ public: // Result properties /// A unique identifier for the event. - QString eventId() const - { - return loadFromJson("event_id"_ls); - } + QString eventId() const { return loadFromJson("event_id"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index 99eb4fc9..f95af223 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -71,10 +71,7 @@ public: // Result properties /// A unique identifier for the event. - QString eventId() const - { - return loadFromJson("event_id"_ls); - } + QString eventId() const { return loadFromJson("event_id"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 179d7a27..51af2c65 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -38,11 +38,7 @@ public: // Result properties /// The full event. - EventPtr event() - - { - return fromJson(jsonData()); - } + EventPtr event() { return fromJson(jsonData()); } }; /*! \brief Get the state identified by the type and key. @@ -103,11 +99,7 @@ public: // Result properties /// The current state of the room - StateEvents events() - - { - return fromJson(jsonData()); - } + StateEvents events() { return fromJson(jsonData()); } }; /*! \brief Get the m.room.member events for the room. diff --git a/lib/csapi/users.h b/lib/csapi/users.h index 772a6365..eab18f6c 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -66,10 +66,7 @@ public: } /// Indicates if the result list has been truncated by the limit. - bool limited() const - { - return loadFromJson("limited"_ls); - } + bool limited() const { return loadFromJson("limited"_ls); } }; template <> diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index 85ab8b41..087ebbbd 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -28,10 +28,7 @@ public: // Result properties /// The TURN server credentials. - QJsonObject data() const - { - return fromJson(jsonData()); - } + QJsonObject data() const { return fromJson(jsonData()); } }; } // namespace Quotient diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index 203742c9..319f82c5 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -34,19 +34,13 @@ public: // Result properties /// The user ID that owns the access token. - QString userId() const - { - return loadFromJson("user_id"_ls); - } + QString userId() const { return loadFromJson("user_id"_ls); } /// Device ID associated with the access token. If no device /// is associated with the access token (such as in the case /// of application services) then this field can be omitted. /// Otherwise this is required. - QString deviceId() const - { - return loadFromJson("device_id"_ls); - } + QString deviceId() const { return loadFromJson("device_id"_ls); } }; } // namespace Quotient -- cgit v1.2.3 From f81aa4d723577ce30518424510e45ef39ff0e29e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 18:59:33 +0200 Subject: Drop an out-of-date comment BaseJob::loadFromJson() does just fine without QStringViews. [skip ci] --- lib/jobs/basejob.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index d33d542e..7ce4b808 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -180,7 +180,7 @@ public: * If there's no top-level JSON object in the response or if there's * no node with the key \p keyName, \p defaultValue is returned. */ - template // Waiting for QStringViews... + template T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const { const auto& jv = jsonData().value(keyName); -- cgit v1.2.3 From 35ce036407e1865402d01070e9680a9cda4f361c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 22:06:42 +0200 Subject: converters.h: (actually) enable QUrl; drop unused types QUrl can now be converted even with QT_NO_URL_CAST_FROM_STRING; and it can also be put to queries. QByteArray did not really need conversion in JSON context; and QJsonObject is/was never used in queries. --- lib/converters.h | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index af6c0192..cc6378e4 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -151,10 +151,17 @@ struct JsonConverter { }; template <> -struct JsonConverter : JsonConverter { - static auto dump(const QUrl& url) // Override on top of that for QString +struct JsonConverter { + static auto load(const QJsonValue& jv) + { + // QT_NO_URL_CAST_FROM_STRING makes this a bit more verbose + QUrl url; + url.setUrl(jv.toString()); + return url; + } + static auto dump(const QUrl& url) { - return JsonConverter::dump(url.toString(QUrl::FullyEncoded)); + return url.toString(QUrl::FullyEncoded); } }; @@ -163,15 +170,6 @@ struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toArray(); } }; -template <> -struct JsonConverter { - static QString dump(const QByteArray& ba) { return ba.constData(); } - static auto load(const QJsonValue& jv) - { - return fromJson(jv).toLatin1(); - } -}; - template <> struct JsonConverter { static QJsonValue dump(const QVariant& v); @@ -304,16 +302,15 @@ namespace _impl { q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); } - inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) + inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v) { - for (const auto& v : vals) - q.addQueryItem(k, v); + q.addQueryItem(k, v.toEncoded()); } - inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) + inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { - for (auto it = vals.begin(); it != vals.end(); ++it) - q.addQueryItem(it.key(), it.value().toString()); + for (const auto& v : vals) + q.addQueryItem(k, v); } // This one is for types that don't have isEmpty() and for all types -- cgit v1.2.3 From 08d5cef8bbbef65c961370d4a9e2c48a6d7281f1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 22:08:11 +0200 Subject: gtad.yaml: use QUrl where API uses 'format: uri' --- gtad/gtad.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 928a1495..58e1909c 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -68,6 +68,9 @@ analyzer: - dateTime: type: QDateTime initializer: QDateTime::fromString("{{defaultValue}}") + - uri: + type: QUrl + initializer: QUrl::fromEncoded("{{defaultValue}}") - //: &QString type: QString initializer: QStringLiteral("{{defaultValue}}") -- cgit v1.2.3 From bf6303c41264d913ca049009034aa948464b8f30 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 22:14:06 +0200 Subject: User::avatar: add const --- lib/user.cpp | 6 +++--- lib/user.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/user.cpp b/lib/user.cpp index 04afed2b..a7e0efd9 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -197,18 +197,18 @@ const Avatar& User::avatarObject(const Room* room) const return d->otherAvatars.try_emplace(mediaId, url).first->second; } -QImage User::avatar(int dimension, const Room* room) +QImage User::avatar(int dimension, const Room* room) const { return avatar(dimension, dimension, room); } -QImage User::avatar(int width, int height, const Room* room) +QImage User::avatar(int width, int height, const Room* room) const { return avatar(width, height, room, [] {}); } QImage User::avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback) + const Avatar::get_callback_t& callback) const { return avatarObject(room).get(connection(), width, height, callback); } diff --git a/lib/user.h b/lib/user.h index e4560843..4ff62951 100644 --- a/lib/user.h +++ b/lib/user.h @@ -99,11 +99,11 @@ public: */ const Avatar& avatarObject(const Room* room = nullptr) const; Q_INVOKABLE QImage avatar(int dimension, - const Quotient::Room* room = nullptr); + const Quotient::Room* room = nullptr) const; Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, - const Quotient::Room* room = nullptr); + const Quotient::Room* room = nullptr) const; QImage avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback); + const Avatar::get_callback_t& callback) const; QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; -- cgit v1.2.3 From bd649c591fa020fde0bd56a63c13025097b831ae Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 22:12:46 +0200 Subject: Update generated files This uses API definition files from https://github.com/matrix-org/matrix-doc/pull/3236, and additionally makes uploadFile>content_uri to have 'format: uri' (as suggested in the PR review). Only use this commit with the next one; alone it breaks the build. --- lib/csapi/content-repo.cpp | 6 +++--- lib/csapi/content-repo.h | 11 ++++------- lib/csapi/definitions/public_rooms_response.h | 2 +- lib/csapi/definitions/request_token_response.h | 2 +- lib/csapi/definitions/wellknown/homeserver.h | 2 +- lib/csapi/definitions/wellknown/identity_server.h | 2 +- lib/csapi/profile.cpp | 2 +- lib/csapi/profile.h | 6 +++--- lib/csapi/pusher.h | 4 ++-- lib/csapi/rooms.h | 2 +- lib/csapi/search.h | 2 +- lib/csapi/users.h | 2 +- 12 files changed, 20 insertions(+), 23 deletions(-) diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index e913bfd1..00bc9706 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -122,7 +122,7 @@ GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, setExpectedContentTypes({ "image/jpeg", "image/png" }); } -auto queryToGetUrlPreview(const QString& url, Omittable ts) +auto queryToGetUrlPreview(const QUrl& url, Omittable ts) { QUrlQuery _q; addParam<>(_q, QStringLiteral("url"), url); @@ -130,7 +130,7 @@ auto queryToGetUrlPreview(const QString& url, Omittable ts) return _q; } -QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, +QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -139,7 +139,7 @@ QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, queryToGetUrlPreview(url, ts)); } -GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable ts) +GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable ts) : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), QStringLiteral("/_matrix/media/r0") % "/preview_url", queryToGetUrlPreview(url, ts)) diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index f3d7309a..28409f5c 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -34,10 +34,7 @@ public: /// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the /// uploaded content. - QString contentUri() const - { - return loadFromJson("content_uri"_ls); - } + QUrl contentUri() const { return loadFromJson("content_uri"_ls); } }; /*! \brief Download content from the content repository. @@ -219,14 +216,14 @@ public: * return a newer version if it does not have the requested version * available. */ - explicit GetUrlPreviewJob(const QString& url, Omittable ts = none); + explicit GetUrlPreviewJob(const QUrl& url, Omittable ts = none); /*! \brief Construct a URL without creating a full-fledged job object * * This function can be used when a URL for GetUrlPreviewJob * is necessary but the job itself isn't. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, + static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable ts = none); // Result properties @@ -239,7 +236,7 @@ public: /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image. /// Omitted if there is no image. - QString ogImage() const { return loadFromJson("og:image"_ls); } + QUrl ogImage() const { return loadFromJson("og:image"_ls); } }; /*! \brief Get the configuration for the content repository. diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 34b447d2..2938b4ec 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -36,7 +36,7 @@ struct PublicRoomsChunk { bool guestCanJoin; /// The URL for the room's avatar, if one is set. - QString avatarUrl; + QUrl avatarUrl; /// The room's join rule. When not present, the room is assumed to /// be `public`. Note that rooms with `invite` join rules are not diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h index f9981100..d5fbbadb 100644 --- a/lib/csapi/definitions/request_token_response.h +++ b/lib/csapi/definitions/request_token_response.h @@ -25,7 +25,7 @@ struct RequestTokenResponse { /// will happen without the client's involvement provided the homeserver /// advertises this specification version in the `/versions` response /// (ie: r0.5.0). - QString submitUrl; + QUrl submitUrl; }; template <> diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index 5cfaca24..b7db4182 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -10,7 +10,7 @@ namespace Quotient { /// Used by clients to discover homeserver information. struct HomeserverInformation { /// The base URL for the homeserver for client-server connections. - QString baseUrl; + QUrl baseUrl; }; template <> diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index 3bd07bd1..885e3d34 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -10,7 +10,7 @@ namespace Quotient { /// Used by clients to discover identity server information. struct IdentityServerInformation { /// The base URL for the identity server for client-server connections. - QString baseUrl; + QUrl baseUrl; }; template <> diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 8436b8e6..745fa488 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -33,7 +33,7 @@ GetDisplayNameJob::GetDisplayNameJob(const QString& userId) false) {} -SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) +SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), QStringLiteral("/_matrix/client/r0") % "/profile/" % userId % "/avatar_url") diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 8bbe4f8c..7f9c9e95 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -73,7 +73,7 @@ public: * \param avatarUrl * The new avatar URL for this user. */ - explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl); + explicit SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl); }; /*! \brief Get the user's avatar URL. @@ -101,7 +101,7 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const { return loadFromJson("avatar_url"_ls); } + QUrl avatarUrl() const { return loadFromJson("avatar_url"_ls); } }; /*! \brief Get this user's profile information. @@ -130,7 +130,7 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const { return loadFromJson("avatar_url"_ls); } + QUrl avatarUrl() const { return loadFromJson("avatar_url"_ls); } /// The user's display name if they have set one, otherwise not present. QString displayname() const diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 13c9ec25..622b0df6 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -21,7 +21,7 @@ public: struct PusherData { /// Required if `kind` is `http`. The URL to use to send /// notifications to. - QString url; + QUrl url; /// The format to use when sending notifications to the Push /// Gateway. QString format; @@ -119,7 +119,7 @@ public: /// Required if `kind` is `http`. The URL to use to send /// notifications to. MUST be an HTTPS URL with a path of /// `/_matrix/push/v1/notify`. - QString url; + QUrl url; /// The format to send notifications in to Push Gateways if the /// `kind` is `http`. The details about what fields the /// homeserver should send to the push gateway are defined in the diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 51af2c65..2620582b 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -175,7 +175,7 @@ public: /// The display name of the user this object is representing. QString displayName; /// The mxc avatar url of the user this object is representing. - QString avatarUrl; + QUrl avatarUrl; }; // Construction/destruction diff --git a/lib/csapi/search.h b/lib/csapi/search.h index b56d9154..3d02752a 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -81,7 +81,7 @@ public: /// Performs a full text search across different categories. QString displayname; /// Performs a full text search across different categories. - QString avatarUrl; + QUrl avatarUrl; }; /// Context for result, if requested. diff --git a/lib/csapi/users.h b/lib/csapi/users.h index eab18f6c..ec186592 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -41,7 +41,7 @@ public: /// The display name of the user, if one exists. QString displayName; /// The avatar url, as an MXC, if one exists. - QString avatarUrl; + QUrl avatarUrl; }; // Construction/destruction -- cgit v1.2.3 From e5a760371a158bec6a70353b96614611adecc4bc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 Aug 2021 22:13:42 +0200 Subject: Update non-generated code to work with QUrls in CS API --- lib/avatar.h | 2 +- lib/user.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/avatar.h b/lib/avatar.h index be125c17..37e1eeef 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -21,7 +21,7 @@ public: Avatar& operator=(Avatar&&); using get_callback_t = std::function; - using upload_callback_t = std::function; + using upload_callback_t = std::function; QImage get(Connection* connection, int dimension, get_callback_t callback) const; diff --git a/lib/user.cpp b/lib/user.cpp index 04afed2b..797576db 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -135,17 +135,17 @@ template inline bool User::doSetAvatar(SourceT&& source) { return d->defaultAvatar.upload( - connection(), source, [this](const QString& contentUri) { + connection(), source, [this](const QUrl& contentUri) { auto* j = connection()->callApi(id(), contentUri); connect(j, &BaseJob::success, this, - [this, newUrl = QUrl(contentUri)] { - if (newUrl == d->defaultAvatar.url()) { - d->defaultAvatar.updateUrl(newUrl); + [this, contentUri] { + if (contentUri == d->defaultAvatar.url()) { + d->defaultAvatar.updateUrl(contentUri); emit defaultAvatarChanged(); } else qCWarning(MAIN) << "User" << id() << "already has avatar URL set to" - << newUrl.toDisplayString(); + << contentUri.toDisplayString(); }); }); } @@ -162,7 +162,7 @@ bool User::setAvatar(QIODevice* source) void User::removeAvatar() { - connection()->callApi(id(), ""); + connection()->callApi(id(), QUrl()); } void User::requestDirectChat() { connection()->requestDirectChat(this); } -- 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(-) 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(+) 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(-) 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(-) 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 f46e19924a1c0a5ac3f1d2a489f8017a5e143fda Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 10 Aug 2021 15:23:08 +0200 Subject: Turn EventStatus from a class to a namespace This wrapper only exists for an enum inside of it and dates back to times when Qt meta-object system did not support free-standing enums. --- lib/eventitem.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/eventitem.h b/lib/eventitem.h index 1986ba77..a5f381f0 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -11,9 +11,9 @@ namespace Quotient { class StateEventBase; -class EventStatus { - Q_GADGET -public: +namespace EventStatus { + Q_NAMESPACE + /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. @@ -32,8 +32,8 @@ public: Hidden = 0x100, //< The event should not be shown in the timeline }; Q_DECLARE_FLAGS(Status, Code) - Q_FLAG(Status) -}; + Q_FLAG_NS(Status) +} // namespace EventStatus class EventItemBase { public: @@ -148,4 +148,4 @@ inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) return d; } } // namespace Quotient -Q_DECLARE_METATYPE(Quotient::EventStatus) +//Q_DECLARE_METATYPE(Quotient::EventStatus) -- cgit v1.2.3 From 14d896c9d659d3d3c4f9c4d6eeb50e59a2a002a9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 21 Aug 2021 11:40:45 +0200 Subject: Cleanup --- lib/eventitem.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/eventitem.h b/lib/eventitem.h index a5f381f0..a70a3c3e 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -148,4 +148,3 @@ inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) return d; } } // namespace Quotient -//Q_DECLARE_METATYPE(Quotient::EventStatus) -- cgit v1.2.3 From 71384a49c3a053e715241172d9d9893bb1742e6b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 19:37:09 +0200 Subject: Mustache: avoid BaseJob::Data It's about to be deprecated in the next commits. --- gtad/operation.cpp.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache index f34c9280..7f692e4a 100644 --- a/gtad/operation.cpp.mustache +++ b/gtad/operation.cpp.mustache @@ -36,7 +36,7 @@ QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}}, { {{#headerParams}} setRequestHeader("{{baseName}}", {{paramName}}.toLatin1()); {{/headerParams}}{{#inlineBody}}{{^propertyMap}}{{^bodyParams?}} - setRequestData(Data({{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? + setRequestData(RequestData({{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}})); {{/bodyParams?}}{{/propertyMap}}{{/inlineBody }}{{^consumesNonJson?}}{{#bodyParams?}} -- cgit v1.2.3 From ed24065f2e9b8fce059c54137c04b790c6ce4fd1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:05:41 +0200 Subject: Regenerate API files --- lib/csapi/account-data.cpp | 4 ++-- lib/csapi/administrative_contact.cpp | 4 ++-- lib/csapi/content-repo.cpp | 2 +- lib/csapi/cross_signing.cpp | 2 +- lib/csapi/filter.cpp | 2 +- lib/csapi/openid.cpp | 2 +- lib/csapi/receipts.cpp | 2 +- lib/csapi/registration.cpp | 8 ++++---- lib/csapi/room_send.cpp | 2 +- lib/csapi/room_state.cpp | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 6a40e908..80deb8f1 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -14,7 +14,7 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, QStringLiteral("/_matrix/client/r0") % "/user/" % userId % "/account_data/" % type) { - setRequestData(Data(toJson(content))); + setRequestData(RequestData(toJson(content))); } QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, @@ -39,7 +39,7 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, QStringLiteral("/_matrix/client/r0") % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) { - setRequestData(Data(toJson(content))); + setRequestData(RequestData(toJson(content))); } QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index fa4f475a..04360299 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -89,7 +89,7 @@ RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( % "/account/3pid/email/requestToken", false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( @@ -99,5 +99,5 @@ RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( % "/account/3pid/msisdn/requestToken", false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 00bc9706..2d82437b 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -22,7 +22,7 @@ UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - setRequestData(Data(content)); + setRequestData(RequestData(content)); addExpectedKey("content_uri"); } diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp index 9bfc026a..ed2b15c0 100644 --- a/lib/csapi/cross_signing.cpp +++ b/lib/csapi/cross_signing.cpp @@ -30,5 +30,5 @@ UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), QStringLiteral("/_matrix/client/r0") % "/keys/signatures/upload") { - setRequestData(Data(toJson(signatures))); + setRequestData(RequestData(toJson(signatures))); } diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index bb3a893f..6b8863cc 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -13,7 +13,7 @@ DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) QStringLiteral("/_matrix/client/r0") % "/user/" % userId % "/filter") { - setRequestData(Data(toJson(filter))); + setRequestData(RequestData(toJson(filter))); addExpectedKey("filter_id"); } diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 3941e9c0..0447db79 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -14,5 +14,5 @@ RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, QStringLiteral("/_matrix/client/r0") % "/user/" % userId % "/openid/request_token") { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 00d1c28a..47b18174 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -15,5 +15,5 @@ PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId) { - setRequestData(Data(toJson(receipt))); + setRequestData(RequestData(toJson(receipt))); } diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 38649e63..c3617bfc 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -44,7 +44,7 @@ RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( % "/register/email/requestToken", false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( @@ -54,7 +54,7 @@ RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( % "/register/msisdn/requestToken", false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } ChangePasswordJob::ChangePasswordJob(const QString& newPassword, @@ -78,7 +78,7 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( % "/account/password/email/requestToken", false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( @@ -89,7 +89,7 @@ RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( % "/account/password/msisdn/requestToken", false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } DeactivateAccountJob::DeactivateAccountJob( diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 63986c56..9fd8cb96 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -14,6 +14,6 @@ SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/send/" % eventType % "/" % txnId) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index e18108ac..37e897fa 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -16,6 +16,6 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); addExpectedKey("event_id"); } -- cgit v1.2.3 From 40548a7995147f3f99212928ae27047de7a79618 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:10:49 +0200 Subject: Deprecate BaseJob::Data The grand plan is to get rid of `BaseJob` and turn job invocations to function calls returning `QFuture`. `RequestData` will stay though, feeding data into those calls. --- lib/jobs/basejob.cpp | 17 ++++++++++------- lib/jobs/basejob.h | 10 ++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 400a9243..239cef28 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -71,8 +71,8 @@ public: // Using an idiom from clang-tidy: // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html - Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data, - bool nt) + Private(HttpVerb v, QString endpoint, const QUrlQuery& q, + RequestData&& data, bool nt) : verb(v) , apiEndpoint(std::move(endpoint)) , requestQuery(q) @@ -109,7 +109,7 @@ public: QString apiEndpoint; QHash requestHeaders; QUrlQuery requestQuery; - Data requestData; + RequestData requestData; bool needsToken; bool inBackground = false; @@ -168,11 +168,11 @@ public: BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, QUrlQuery {}, Data {}, needsToken) + : BaseJob(verb, name, endpoint, QUrlQuery {}, RequestData {}, needsToken) {} BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const QUrlQuery &query, Data&& data, bool needsToken) + const QUrlQuery& query, RequestData&& data, bool needsToken) : d(new Private(verb, endpoint, query, std::move(data), needsToken)) { setObjectName(name); @@ -224,9 +224,12 @@ void BaseJob::setRequestQuery(const QUrlQuery& query) d->requestQuery = query; } -const BaseJob::Data& BaseJob::requestData() const { return d->requestData; } +const RequestData& BaseJob::requestData() const { return d->requestData; } -void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); } +void BaseJob::setRequestData(RequestData&& data) +{ + std::swap(d->requestData, data); +} const QByteArrayList& BaseJob::expectedContentTypes() const { diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 7ce4b808..7750fb8b 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -72,7 +72,8 @@ public: }; Q_ENUM(StatusCode) - using Data = RequestData; + using Data Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead") // + = RequestData; /*! * This structure stores the status of a server call job. The status @@ -125,7 +126,8 @@ public: BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken = true); BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const QUrlQuery& query, Data&& data = {}, bool needsToken = true); + const QUrlQuery& query, RequestData&& data = {}, + bool needsToken = true); QUrl requestUrl() const; bool isBackground() const; @@ -330,8 +332,8 @@ protected: void setRequestHeaders(const headers_t& headers); const QUrlQuery& query() const; void setRequestQuery(const QUrlQuery& query); - const Data& requestData() const; - void setRequestData(Data&& data); + const RequestData& requestData() const; + void setRequestData(RequestData&& data); const QByteArrayList& expectedContentTypes() const; void addExpectedContentType(const QByteArray& contentType); void setExpectedContentTypes(const QByteArrayList& contentTypes); -- cgit v1.2.3 From c50420a0f2df7a7bf291312c38ac43e2c9f58141 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:15:04 +0200 Subject: Drop QMatrixClient namespace alias --- lib/avatar.h | 2 -- lib/jobs/requestdata.h | 2 -- lib/logging.h | 2 -- 3 files changed, 6 deletions(-) diff --git a/lib/avatar.h b/lib/avatar.h index 37e1eeef..d4634aea 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -42,5 +42,3 @@ private: std::unique_ptr d; }; } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 21657631..4f05e5ff 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -35,5 +35,3 @@ private: std::unique_ptr _source; }; } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; diff --git a/lib/logging.h b/lib/logging.h index 264215e1..1d1394e8 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -67,8 +67,6 @@ inline qint64 profilerMinNsecs() * 1000; } } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) { -- cgit v1.2.3 From c26015503aa0fbca37abdfc4870ac94bb7befeee Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:19:15 +0200 Subject: Drop other stuff deprecated pre- or early 0.6 BaseJob: StatusCode::JsonParseError Connection: resolved() and reconnected() signals; roomMap(); postReceipt() User: bridged() and rawName() ConnectionData: setHost() and setPort() StateEventBase: prev_content() --- lib/connection.cpp | 21 --------------------- lib/connection.h | 45 --------------------------------------------- lib/connectiondata.cpp | 12 ------------ lib/connectiondata.h | 4 ---- lib/events/stateevent.h | 4 ---- lib/jobs/basejob.h | 2 -- lib/user.cpp | 4 ---- lib/user.h | 18 +----------------- 8 files changed, 1 insertion(+), 109 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 7dd04aaa..222c3b71 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -314,8 +314,6 @@ void Connection::resolveServer(const QString& mxid) setHomeserver(maybeBaseUrl); } Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver() - connect(d->loginFlowsJob, &BaseJob::success, this, - &Connection::resolved); connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { qCWarning(MAIN) << "Homeserver base URL sanity check failed"; emit resolveError(tr("The homeserver doesn't seem to be working")); @@ -795,11 +793,6 @@ void Connection::stopSync() QString Connection::nextBatchToken() const { return d->data->lastEvent(); } -PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) -{ - return callApi(room->id(), "m.read", event->id()); -} - JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { @@ -1239,20 +1232,6 @@ int Connection::millisToReconnect() const return d->syncJob ? d->syncJob->millisToRetry() : 0; } -QHash, Room*> Connection::roomMap() const -{ - // Copy-on-write-and-remove-elements is faster than copying elements one by - // one. - QHash, Room*> roomMap = d->roomMap; - for (auto it = roomMap.begin(); it != roomMap.end();) { - if (it.value()->joinState() == JoinState::Leave) - it = roomMap.erase(it); - else - ++it; - } - return roomMap; -} - QVector Connection::allRooms() const { QVector result; diff --git a/lib/connection.h b/lib/connection.h index a7a071f3..ecbb1a19 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -138,15 +138,6 @@ public: explicit Connection(const QUrl& server, QObject* parent = nullptr); ~Connection() override; - /// Get all Invited and Joined rooms - /*! - * \return a hashmap from a composite key - room name and whether - * it's an Invite rather than Join - to room pointers - * \sa allRooms, rooms, roomsWithTag - */ - [[deprecated("Use allRooms(), roomsWithTag() or rooms(joinStates) instead")]] - QHash, Room*> roomMap() const; - /// Get all rooms known within this Connection /*! * This includes Invite, Join and Leave rooms, in no particular order. @@ -524,30 +515,12 @@ public Q_SLOTS: */ void assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId); - /*! \deprecated Use loginWithPassword instead */ - void connectToServer(const QString& userId, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}) - { - loginWithPassword(userId, password, initialDeviceName, deviceId); - } - /*! \deprecated - * Use assumeIdentity() if you have an access token or - * loginWithToken() if you have a login token. - */ - void connectWithToken(const QString& userId, const QString& accessToken, - const QString& deviceId) - { - assumeIdentity(userId, accessToken, deviceId); - } /// Explicitly request capabilities from the server void reloadCapabilities(); /// Find out if capabilites are still loading from the server bool loadingCapabilities() const; - /** @deprecated Use stopSync() instead */ - void disconnectFromServer() { stopSync(); } void logout(); void sync(int timeout = -1); @@ -662,24 +635,7 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); - // Old API that will be abolished any time soon. DO NOT USE. - - /** @deprecated Use callApi() or Room::postReceipt() instead - */ - virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event); - Q_SIGNALS: - /** - * @deprecated - * This was a signal resulting from a successful resolveServer(). - * Since Connection now provides setHomeserver(), the HS URL - * may change even without resolveServer() invocation. Use - * loginFLowsChanged() instead of resolved(). You can also use - * loginWith*() and assumeIdentity() without the HS URL set in - * advance (i.e. without calling resolveServer), as they trigger - * server name resolution from MXID if the server URL is not valid. - */ - void resolved(); void resolveError(QString error); void homeserverChanged(QUrl baseUrl); @@ -687,7 +643,6 @@ Q_SIGNALS: void capabilitiesLoaded(); void connected(); - void reconnected(); //< \deprecated Use connected() instead void loggedOut(); /** Login data or state have changed * diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index e54d909b..87ad4577 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -118,18 +118,6 @@ void ConnectionData::setToken(QByteArray token) d->accessToken = std::move(token); } -void ConnectionData::setHost(QString host) -{ - d->baseUrl.setHost(host); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - -void ConnectionData::setPort(int port) -{ - d->baseUrl.setPort(port); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - const QString& ConnectionData::deviceId() const { return d->deviceId; } const QString& ConnectionData::userId() const { return d->userId; } diff --git a/lib/connectiondata.h b/lib/connectiondata.h index 7dd96f26..e16a2dac 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -31,10 +31,6 @@ public: void setBaseUrl(QUrl baseUrl); void setToken(QByteArray accessToken); - [[deprecated("Use setBaseUrl() instead")]] - void setHost(QString host); - [[deprecated("Use setBaseUrl() instead")]] - void setPort(int port); void setDeviceId(const QString& deviceId); void setUserId(const QString& userId); void setNeedsToken(const QString& requestName); diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 1415f709..bc414a5f 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -100,10 +100,6 @@ public: visitor(_content); editJson()[ContentKeyL] = _content.toJson(); } - [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const - { - return prevContent(); - } const ContentT* prevContent() const { return _prev ? &_prev->content : nullptr; diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 7750fb8b..835cd822 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -52,8 +52,6 @@ public: IncorrectRequestError = IncorrectRequest, IncorrectResponse, IncorrectResponseError = IncorrectResponse, - JsonParseError //< \deprecated Use IncorrectResponse instead - = IncorrectResponse, TooManyRequests, TooManyRequestsError = TooManyRequests, RateLimited = TooManyRequests, diff --git a/lib/user.cpp b/lib/user.cpp index a4abed37..60920a59 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -93,8 +93,6 @@ QString User::name(const Room* room) const return room ? room->memberName(id()) : d->defaultName; } -QString User::rawName(const Room* room) const { return name(room); } - void User::rename(const QString& newName) { const auto actualNewName = sanitized(newName); @@ -185,8 +183,6 @@ QString User::fullName(const Room* room) const return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } -QString User::bridged() const { return {}; } - const Avatar& User::avatarObject(const Room* room) const { if (!room) diff --git a/lib/user.h b/lib/user.h index 4ff62951..78b72bf2 100644 --- a/lib/user.h +++ b/lib/user.h @@ -22,7 +22,6 @@ class User : public QObject { Q_PROPERTY(QString name READ name NOTIFY defaultNameChanged) Q_PROPERTY(QString displayName READ displayname NOTIFY defaultNameChanged STORED false) Q_PROPERTY(QString fullName READ fullName NOTIFY defaultNameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY defaultNameChanged STORED false) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) public: @@ -40,18 +39,10 @@ public: * This may be empty if the user didn't choose the name or cleared * it. If the user is bridged, the bridge postfix (such as '(IRC)') * is stripped out. No disambiguation for the room is done. - * \sa displayName, rawName + * \sa displayName */ QString name(const Room* room = nullptr) const; - /** Get the user name along with the bridge postfix - * This function is similar to name() but appends the bridge postfix - * (such as '(IRC)') to the user name. No disambiguation is done. - * \sa name, displayName - */ - [[deprecated("Bridge postfixes exist no more, use name() instead")]] - QString rawName(const Room* room = nullptr) const; - /** Get the displayed user name * When \p room is null, this method returns result of name() if * the name is non-empty; otherwise it returns user id. @@ -70,13 +61,6 @@ public: */ QString fullName(const Room* room = nullptr) const; - /** - * Returns the name of bridge the user is connected from or empty. - */ - [[deprecated("Bridged status is no more supported; this always returns" - " an empty string")]] - QString bridged() const; - /** Whether the user is a guest * As of now, the function relies on the convention used in Synapse * that guests and only guests have all-numeric IDs. This may or -- 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 ++-- lib/room.h | 22 +++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) 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 diff --git a/lib/room.h b/lib/room.h index 52ba2eab..de0c7504 100644 --- a/lib/room.h +++ b/lib/room.h @@ -192,7 +192,7 @@ public: QList membersLeft() const; Q_INVOKABLE QList users() const; - [[deprecated("Use safeMemberNames() or htmlSafeMemberNames() instead")]] + Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") // QStringList memberNames() const; QStringList safeMemberNames() const; QStringList htmlSafeMemberNames() const; @@ -235,13 +235,12 @@ public: /** * \brief Check the join state of a given user in this room * - * \deprecated Use memberState and check against a mask - * * \note Banned and invited users are not tracked separately for now (Leave * will be returned for them). * * \return Join if the user is a room member; Leave otherwise */ + Q_DECL_DEPRECATED_X("Use memberState and check against the mask") // Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; //! \brief Check the join state of a given user in this room @@ -254,18 +253,11 @@ public: //! \sa safeMemberName, htmlSafeMemberName Q_INVOKABLE QString memberName(const QString& mxId) const; - /*! - * \brief Get a disambiguated name for the given user in the room context - * - * \deprecated use safeMemberName() instead - */ + //! \brief Get a disambiguated name for the given user in the room context + Q_DECL_DEPRECATED_X("Use safeMemberName() instead") Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; - /*! - * \brief Get a disambiguated name for a user with this id in the room - * context - * - * \deprecated use safeMemberName() instead - */ + //! \brief Get a disambiguated name for a user with this id in the room + Q_DECL_DEPRECATED_X("Use safeMemberName() instead") Q_INVOKABLE QString roomMembername(const QString& userId) const; /*! @@ -552,7 +544,7 @@ public Q_SLOTS: QString postFile(const QString& plainText, EventContent::TypedBase* content); #if QT_VERSION_MAJOR < 6 - /// \deprecated Use postFile(QString, MessageEventType, EventContent) instead + Q_DECL_DEPRECATED_X("Use postFile(QString, MessageEventType, EventContent)") // QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); #endif -- cgit v1.2.3 From 395f3758e810fed6cd276a356fd5f0e955c5477e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:22:36 +0200 Subject: Settings: no more setToken() and accessToken(); deprecate clearAccessToken() Access tokens should be stored with Qt Keychain that's about to come; and these methods were deprecated since before 0.5. --- lib/settings.cpp | 13 ------------- lib/settings.h | 8 +------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/lib/settings.cpp b/lib/settings.cpp index 1d36db27..ed9082b0 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -126,19 +126,6 @@ void AccountSettings::setHomeserver(const QUrl& url) QString AccountSettings::userId() const { return group().section('/', -1); } -QString AccountSettings::accessToken() const -{ - return value(AccessTokenKey).toString(); -} - -void AccountSettings::setAccessToken(const QString& accessToken) -{ - qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, do it manually or contribute to share " - "QtKeychain logic to libQuotient."; - setValue(AccessTokenKey, accessToken); -} - void AccountSettings::clearAccessToken() { legacySettings.remove(AccessTokenKey); diff --git a/lib/settings.h b/lib/settings.h index 84c54802..3f4dc123 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -131,8 +131,6 @@ class AccountSettings : public SettingsGroup { QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName) QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) - /** \deprecated \sa setAccessToken */ - Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle WRITE setEncryptionAccountPickle) public: @@ -147,11 +145,7 @@ public: QUrl homeserver() const; void setHomeserver(const QUrl& url); - /** \deprecated \sa setToken */ - QString accessToken() const; - /** \deprecated Storing accessToken in QSettings is unsafe, - * see quotient-im/Quaternion#181 */ - void setAccessToken(const QString& accessToken); + Q_DECL_DEPRECATED_X("Access tokens are not stored in QSettings any more") Q_INVOKABLE void clearAccessToken(); QByteArray encryptionAccountPickle(); -- cgit v1.2.3 From fd42d1dbd29800ef53ab9997c948f39a92aa8bff Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:23:42 +0200 Subject: RoomEvent: drop timestamp() Use originTimestamp(); the corresponding Q_PROPERTY was not renamed (in error) so it is now. --- lib/events/roomevent.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 3abd56c0..3174764f 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -14,7 +14,9 @@ class RedactionEvent; class RoomEvent : public Event { Q_GADGET Q_PROPERTY(QString id READ id) - Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) + //! \deprecated Use originTimestamp instead + Q_PROPERTY(QDateTime timestamp READ originTimestamp CONSTANT) + Q_PROPERTY(QDateTime originTimestamp READ originTimestamp CONSTANT) Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString senderId READ senderId CONSTANT) Q_PROPERTY(QString redactionReason READ redactionReason) @@ -32,9 +34,6 @@ public: QString id() const; QDateTime originTimestamp() const; - [[deprecated("Use originTimestamp()")]] QDateTime timestamp() const { - return originTimestamp(); - } QString roomId() const; QString senderId() const; //! \brief Determine whether the event has been replaced -- cgit v1.2.3 From 93cee89f9a99e51275ac9cd304180499b0543eea Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:45:53 +0200 Subject: Fix building with MSVC --- lib/jobs/basejob.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 835cd822..29443c53 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -70,7 +70,10 @@ public: }; Q_ENUM(StatusCode) - using Data Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead") // + using Data +#ifndef Q_CC_MSVC + Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead") +#endif = RequestData; /*! -- cgit v1.2.3 From 3cbc13a33c81a75e18c415bd31cc2156461ffa1f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 22 Aug 2021 20:58:18 +0200 Subject: User::isGuest(): fix a clazy warning --- lib/user.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/user.cpp b/lib/user.cpp index a4abed37..2706b2c0 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -82,7 +82,7 @@ bool User::isGuest() const Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@')); auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(), [](QChar c) { return c.isDigit(); }); - Q_ASSERT(it != d->id.end()); + Q_ASSERT(it != d->id.cend()); return *it == ':'; } -- 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 +++++++++++++++++------------ lib/room.h | 7 +++++-- 2 files changed, 22 insertions(+), 14 deletions(-) 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); } } diff --git a/lib/room.h b/lib/room.h index de0c7504..179295d0 100644 --- a/lib/room.h +++ b/lib/room.h @@ -240,13 +240,16 @@ public: * * \return Join if the user is a room member; Leave otherwise */ - Q_DECL_DEPRECATED_X("Use memberState and check against the mask") // + Q_DECL_DEPRECATED_X("Use isMember() instead") Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; //! \brief Check the join state of a given user in this room //! //! \return the given user's state with respect to the room - Q_INVOKABLE Quotient::Membership memberState(User* user) const; + Q_INVOKABLE Quotient::Membership memberState(const QString& userId) const; + + //! Check whether a user with the given id is a member of the room + Q_INVOKABLE bool isMember(const QString& userId) const; //! \brief Get a display name (without disambiguation) for the given member //! -- cgit v1.2.3 From 8d21c8a6579ad9c63e331ffb42e3ed81b9c73caf Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 24 Aug 2021 23:59:48 +0200 Subject: Add AccountRegistry Basic session management class; Created from Quaternion's AccountRegistry and NeoChat's AccountListModel. The connections can be accessed by the user's id, this technically limits it to one connection for each matrix account. --- CMakeLists.txt | 1 + lib/accountregistry.cpp | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/accountregistry.h | 45 +++++++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 lib/accountregistry.cpp create mode 100644 lib/accountregistry.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 49105389..3c6e7548 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,7 @@ list(APPEND lib_SRCS lib/util.cpp lib/encryptionmanager.cpp lib/eventitem.cpp + lib/accountregistry.cpp lib/events/event.cpp lib/events/roomevent.cpp lib/events/stateevent.cpp diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp new file mode 100644 index 00000000..3a022f14 --- /dev/null +++ b/lib/accountregistry.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Kitsune Ral +// SPDX-FileCopyrightText: Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "accountregistry.h" + +#include "connection.h" + +using namespace Quotient; + +void AccountRegistry::add(Connection* c) +{ + if (m_accounts.contains(c)) + return; + beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size()); + m_accounts += c; + endInsertRows(); +} + +void AccountRegistry::drop(Connection* c) +{ + beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c)); + m_accounts.removeOne(c); + endRemoveRows(); + Q_ASSERT(!m_accounts.contains(c)); +} + +bool AccountRegistry::isLoggedIn(const QString &userId) const +{ + return std::any_of(m_accounts.cbegin(), m_accounts.cend(), + [&userId](Connection* a) { return a->userId() == userId; }); +} + +bool AccountRegistry::contains(Connection *c) const +{ + return m_accounts.contains(c); +} + +AccountRegistry::AccountRegistry() +{} + +QVariant AccountRegistry::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= m_accounts.count()) { + return {}; + } + + auto account = m_accounts[index.row()]; + + if (role == ConnectionRole) { + return QVariant::fromValue(account); + } + + return {}; +} + +int AccountRegistry::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_accounts.count(); +} + +QHash AccountRegistry::roleNames() const +{ + return {{ConnectionRole, "connection"}}; +} + +bool AccountRegistry::isEmpty() const +{ + return m_accounts.isEmpty(); +} + +int AccountRegistry::count() const +{ + return m_accounts.count(); +} + +const QVector AccountRegistry::accounts() const +{ + return m_accounts; +} + +Connection* AccountRegistry::get(const QString& userId) +{ + for (const auto &connection : m_accounts) { + if(connection->userId() == userId) { + return connection; + } + } + return nullptr; +} \ No newline at end of file diff --git a/lib/accountregistry.h b/lib/accountregistry.h new file mode 100644 index 00000000..e87da3e8 --- /dev/null +++ b/lib/accountregistry.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral +// SPDX-FileCopyrightText: Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include + +namespace Quotient { +class Connection; + +class AccountRegistry : public QAbstractListModel { + Q_OBJECT +public: + enum EventRoles { + ConnectionRole = Qt::UserRole + 1, + }; + + static AccountRegistry &instance() { + static AccountRegistry _instance; + return _instance; + } + + const QVector accounts() const; + void add(Connection* a); + void drop(Connection* a); + bool isLoggedIn(const QString& userId) const; + bool isEmpty() const; + int count() const; + bool contains(Connection*) const; + Connection* get(const QString& userId); + + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + [[nodiscard]] QHash roleNames() const override; + +private: + AccountRegistry(); + + QVector m_accounts; +}; +} \ No newline at end of file -- cgit v1.2.3 From f005f3b69651bd6d6f58879804e1281f6c08177a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 30 Aug 2021 12:33:56 +0200 Subject: SettingsGroup, AccountSettings: simplify constructors The parameter packs intended to pass organisation/application names to QSettings never worked that way since Quotient::Settings didn't have that parameter pack in its constructor. On the other hand, setting organisation/application name using static methods before constructing the first settings object has been working just fine so far. If someone needs to create a settings object with customised org/app name for some reason (sneaking settings from other apps?), those can be brought back in a working manner and without breaking API/ABI even. --- lib/settings.h | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/settings.h b/lib/settings.h index 3f4dc123..efd0d714 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -78,9 +78,8 @@ protected: class SettingsGroup : public Settings { public: - template - explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs) - : Settings(std::forward(qsettingsArgs)...) + explicit SettingsGroup(QString path, QObject* parent = nullptr) + : Settings(parent) , groupPath(std::move(path)) {} @@ -134,10 +133,8 @@ class AccountSettings : public SettingsGroup { Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle WRITE setEncryptionAccountPickle) public: - template - explicit AccountSettings(const QString& accountId, ArgTs&&... qsettingsArgs) - : SettingsGroup("Accounts/" + accountId, - std::forward(qsettingsArgs)...) + explicit AccountSettings(const QString& accountId, QObject* parent = nullptr) + : SettingsGroup("Accounts/" + accountId, parent) {} QString userId() const; -- 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 +++++ lib/room.h | 2 ++ 2 files changed, 7 insertions(+) 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 diff --git a/lib/room.h b/lib/room.h index 179295d0..d3a7466d 100644 --- a/lib/room.h +++ b/lib/room.h @@ -85,6 +85,7 @@ class Room : public QObject { Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) + Q_PROPERTY(QString displayNameForHtml READ displayNameForHtml NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) @@ -183,6 +184,7 @@ public: //! Get a list of both canonical and alternative aliases QStringList aliases() const; QString displayName() const; + QString displayNameForHtml() const; QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; -- cgit v1.2.3 From e2de07628f61c565ac8c85fa3aae84a5fa6feba3 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 15:06:46 +0200 Subject: Add functions and macros to query for Quotient's version --- CMakeLists.txt | 4 ++++ lib/util.cpp | 20 ++++++++++++++++++++ lib/util.h | 5 +++++ 3 files changed, 29 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c6e7548..aca1f982 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,6 +275,10 @@ file(GLOB_RECURSE api_SRCS ${add_CONFIGURE_DEPENDS} ${FULL_CSAPI_DIR}/*.cpp) add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) + +target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + ${PROJECT_NAME}_VERSION_MINOR=${PROJECT_VERSION_MINOR} ${PROJECT_NAME}_VERSION_PATCH=${PROJECT_VERSION_PATCH} + ${PROJECT_NAME}_VERSION_STRING=\"${PROJECT_VERSION}\") if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) diff --git a/lib/util.cpp b/lib/util.cpp index 904bfd5a..3de1d169 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -119,6 +119,26 @@ QString Quotient::serverPart(const QString& mxId) return parser.match(mxId).captured(1); } +QString Quotient::versionString() +{ + return QStringLiteral(Quotient_VERSION_STRING); +} + +int Quotient::majorVersion() +{ + return Quotient_VERSION_MAJOR; +} + +int Quotient::minorVersion() +{ + return Quotient_VERSION_MINOR; +} + +int Quotient::patchVersion() +{ + return Quotient_VERSION_PATCH; +} + // Tests for function_traits<> using namespace Quotient; diff --git a/lib/util.h b/lib/util.h index 78fc9ab7..5bfe6841 100644 --- a/lib/util.h +++ b/lib/util.h @@ -318,4 +318,9 @@ qreal stringToHueF(const QString& s); /** Extract the serverpart from MXID */ QString serverPart(const QString& mxId); + +QString versionString(); +int majorVersion(); +int minorVersion(); +int patchVersion(); } // namespace Quotient -- cgit v1.2.3 From 6be4436494506f2fa5dbbdc633577e059a422bfe Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 24 Aug 2021 14:04:49 +0200 Subject: Add Windows, CodeQL Windows and CodeQL snippets picked from Quaternion --- .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20100e5f..87bb5149 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,21 @@ jobs: matrix: os: [ubuntu-20.04, macos-10.15] compiler: [ GCC, Clang ] + platform: [ '' ] qt-version: [ '5.12.10' ] + qt-arch: [ '' ] # Not using binary values here, to make the job captions more readable e2ee: [ '' ] update-api: [ '', 'update-api' ] exclude: - os: macos-10.15 compiler: GCC + include: + - os: windows-2019 + compiler: MSVC + platform: x64 + qt-version: '5.12.10' + qt-arch: win64_msvc2017_64 steps: - uses: actions/checkout@v2 @@ -36,15 +44,16 @@ jobs: uses: actions/cache@v2 with: path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}-Qt${{ matrix.qt-version }}-cache + key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache - name: Install Qt uses: jurplel/install-qt-action@v2.11.1 with: version: ${{ matrix.qt-version }} + arch: ${{ matrix.qt-arch }} cached: ${{ steps.cache-qt.outputs.cache-hit }} - - name: Install Ninja (macOS) + - name: Install Ninja (macOS/Windows) if: ${{ !startsWith(matrix.os, 'ubuntu') }} uses: seanmiddleditch/gha-setup-ninja@v3 @@ -60,9 +69,13 @@ jobs: if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-9'; fi echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV - else + elif [[ '${{ matrix.compiler }}' == 'Clang' ]]; then echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV + if [[ '${{ runner.os }}' == 'Linux' ]]; then + # Do CodeQL analysis on one of Linux branches + echo "CODEQL_ANALYSIS=true" >>$GITHUB_ENV + fi fi if grep -q 'refs/tags' <<<'${{ github.ref }}'; then VERSION="$(git describe --tags)" @@ -76,6 +89,12 @@ jobs: -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build + - name: Setup MSVC environment + uses: ilammy/msvc-dev-cmd@v1 + if: matrix.compiler == 'MSVC' + with: + arch: ${{ matrix.platform }} + - name: Build and install olm if: matrix.e2ee run: | @@ -97,6 +116,16 @@ jobs: -DGTAD_PATH=${{ runner.workspace }}/gtad/gtad" \ >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV + + - name: Initialize CodeQL tools + if: env.CODEQL_ANALYSIS + uses: github/codeql-action/init@v1 + with: + languages: cpp + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Configure libQuotient run: cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} @@ -117,3 +146,7 @@ jobs: run: | [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually + + - name: Perform CodeQL analysis + if: env.CODEQL_ANALYSIS + uses: github/codeql-action/analyze@v1 -- cgit v1.2.3 From c813a084b6dc8206a9c7305bbb236e23edcb49e5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 24 Aug 2021 18:38:45 +0200 Subject: Fix building with MSVC Turned out it was broken, and I was looking the other way. --- lib/jobs/basejob.cpp | 4 ++-- lib/quotient_common.h | 31 +++++++++++++------------------ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 239cef28..6346db9d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -156,8 +156,8 @@ public: { // FIXME: use std::array {} when Apple stdlib gets deduction guides for it static const auto verbs = - to_array({ QStringLiteral("GET"), QStringLiteral("PUT"), - QStringLiteral("POST"), QStringLiteral("DELETE") }); + make_array(QStringLiteral("GET"), QStringLiteral("PUT"), + QStringLiteral("POST"), QStringLiteral("DELETE")); const auto verbWord = verbs.at(size_t(verb)); return verbWord % ' ' % (reply ? reply->url().toString(QUrl::RemoveQuery) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index d225ad63..bb2e6a6b 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -10,22 +10,17 @@ namespace Quotient { Q_NAMESPACE -namespace impl { - template - constexpr std::array, N> - to_array_impl(T (&&a)[N], std::index_sequence) - { - return { {std::move(a[I])...} }; - } -} // std::array {} needs explicit template parameters on macOS because -// Apple stdlib doesn't have deduction guides for std::array; to alleviate that, -// to_array() is borrowed from C++20 (thanks to cppreference for the possible -// implementation: https://en.cppreference.com/w/cpp/container/array/to_array) -template -constexpr auto to_array(T (&& items)[N]) +// Apple stdlib doesn't have deduction guides for std::array. C++20 has +// to_array() but that can't be borrowed, this time because of MSVC: +// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038 +// Therefore a simpler (but also slightly more wobbly - it resolves the element +// type using std::common_type<>) make_array facility is implemented here. +template +constexpr auto make_array(Ts&&... items) { - return impl::to_array_impl(std::move(items), std::make_index_sequence{}); + return std::array, sizeof...(items)>( + { std::forward(items)... }); } // TODO: code like this should be generated from the CS API definition @@ -49,9 +44,9 @@ enum class Membership : unsigned int { Q_DECLARE_FLAGS(MembershipMask, Membership) Q_FLAG_NS(MembershipMask) -constexpr inline auto MembershipStrings = to_array( +constexpr inline auto MembershipStrings = make_array( // The order MUST be the same as the order in the original enum - { "join", "leave", "invite", "knock", "ban" }); + "join", "leave", "invite", "knock", "ban"); //! \brief Local user join-state names //! @@ -68,10 +63,10 @@ enum class JoinState : std::underlying_type_t { Q_DECLARE_FLAGS(JoinStates, JoinState) Q_FLAG_NS(JoinStates) -constexpr inline auto JoinStateStrings = to_array({ +constexpr inline auto JoinStateStrings = make_array( MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ -}); +); //! \brief Network job running policy flags //! -- cgit v1.2.3 From 66795dbc90e5013eb5ddf941f7e462053a3d5229 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 24 Aug 2021 19:29:27 +0200 Subject: Fix bin path differences between POSIX and Windows --- .github/workflows/ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87bb5149..9d985fca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,14 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Configure libQuotient - run: cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} + run: | + if [[ '${{ runner.os }}' == 'Windows' ]]; then + BIN_DIR=. + else + BIN_DIR=bin + fi + echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV + cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} - name: Regenerate API code if: matrix.update-api @@ -137,7 +144,7 @@ jobs: - name: Build and install libQuotient run: | cmake --build build --target install - ls ~/.local/bin/quotest + ls ~/.local/$BIN_DIR/quotest - name: Run tests env: -- cgit v1.2.3 From e150cdaf11800220f2cac0b6b348ebf8583514b0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 1 Sep 2021 14:10:28 +0200 Subject: Add update-api on Windows pipeline --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d985fca..c90037ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,12 @@ jobs: platform: x64 qt-version: '5.12.10' qt-arch: win64_msvc2017_64 + - os: windows-2019 + compiler: MSVC + platform: x64 + qt-version: '5.12.10' + qt-arch: win64_msvc2017_64 + update-api: update-api steps: - uses: actions/checkout@v2 -- cgit v1.2.3 From 339db5a0e3e458f12beeec57116514e88f8ab354 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 1 Sep 2021 15:34:31 +0200 Subject: Change GTAD/matrix-doc paths `${{ runner.workspace }}` is, unfortunately, not portable to Windows. --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c90037ea..670153eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,7 @@ jobs: - name: Build and install olm if: matrix.e2ee run: | - cd ${{ runner.workspace }} + cd .. git clone https://gitlab.matrix.org/matrix-org/olm.git cmake -S olm -B olm/build $CMAKE_ARGS cmake --build olm/build --target install @@ -113,13 +113,13 @@ jobs: - name: Pull CS API and build GTAD if: matrix.update-api run: | - cd ${{ runner.workspace }} + cd .. git clone https://github.com/matrix-org/matrix-doc.git git clone --recursive https://github.com/KitsuneRal/gtad.git - cmake -S gtad -B gtad $CMAKE_ARGS + cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF cmake --build gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc \ - -DGTAD_PATH=${{ runner.workspace }}/gtad/gtad" \ + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ + -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV -- cgit v1.2.3 From db9dd0ea43f2140e0df72d05861939ee1cb98053 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 1 Sep 2021 17:06:41 +0200 Subject: CMakeLists: fix resolving gtad on Windows --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 49105389..5e5c1932 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,7 +176,7 @@ set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) if (GTAD_PATH AND MATRIX_DOC_PATH) - get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) + get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" PROGRAM PROGRAM_ARGS GTAD_ARGS) if (EXISTS ${ABS_GTAD_PATH}) get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/data/api" REALPATH) if (NOT IS_DIRECTORY ${ABS_API_DEF_PATH}) @@ -227,6 +227,7 @@ if (API_GENERATION_ENABLED) old_sync.yaml- room_initial_sync.yaml- # deprecated key_backup.yaml- # immature and buggy in terms of API definition sync.yaml- # we have a better handcrafted implementation + ${GTAD_ARGS} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/lib SOURCES gtad/gtad.yaml gtad/data.h.mustache -- cgit v1.2.3 From 87d05252acc4ed94d24150eabb2dd1a1f700f46f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 1 Sep 2021 17:08:48 +0200 Subject: CMakeLists: allow to pass clang-format options in CLANG_FORMAT This supersedes passing clang-format options in a separate CLANG_FORMAT_ARGS CMake variable. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e5c1932..22951faa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,7 +198,7 @@ if (API_GENERATION_ENABLED) if (NOT CLANG_FORMAT) set(CLANG_FORMAT clang-format) endif() - get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM) + get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM PROGRAM_ARGS CLANG_FORMAT_ARGS) if (ABS_CLANG_FORMAT) set(API_FORMATTING_ENABLED 1) message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") -- cgit v1.2.3 From 92efa87eff1b35bc857633af20ea8987d926dd5e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 1 Sep 2021 21:51:24 +0200 Subject: Drop .appveyor.yml --- .appveyor.yml | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index fa031ed8..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,37 +0,0 @@ -image: Visual Studio 2017 - -environment: - CMAKE_ARGS: '-G "NMake Makefiles JOM" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo' - matrix: - - QTDIR: C:\Qt\5.13\msvc2017 # Fresh Qt, 32-bit - VCVARS: "vcvars32.bat" - PLATFORM: x86 - - QTDIR: C:\Qt\5.13\msvc2017_64 # Fresh Qt, 64-bit - VCVARS: "vcvars64.bat" - PLATFORM: - -init: -- call "%QTDIR%\bin\qtenv2.bat" -- set PATH=C:\Qt\Tools\QtCreator\bin;%PATH% -- call "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\%VCVARS%" %PLATFORM% -- cd /D "%APPVEYOR_BUILD_FOLDER%" - -before_build: -- git submodule update --init --recursive - -build_script: -- cmake %CMAKE_ARGS% -H. -Bbuild -- cmake --build build - -#after_build: -#- cmake --build build --target install -#- 7z a quotient.zip "%DEPLOY_DIR%\" - -# Uncomment this to connect to the AppVeyor build worker -#on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - -test: off - -#artifacts: -#- path: quotient.zip -- cgit v1.2.3 From f14b04157a9cacaf60f815f164939cc42dad04da Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 2 Sep 2021 19:42:24 +0200 Subject: Require CMake 3.16; drop qmake; use C++20 and newer compilers Also, refresh the documentation a bit. --- CMakeLists.txt | 30 +++++++------- CONTRIBUTING.md | 2 +- README.md | 125 +++++++++++++++++++++++++------------------------------- 3 files changed, 72 insertions(+), 85 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 62fa43de..c0a39932 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.16) if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() @@ -207,15 +207,14 @@ if (API_GENERATION_ENABLED) message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") endif () - if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.12.0") - # We use globbing with CONFIGURE_DEPENDS to produce two file lists: - # one of all API files for clang-format and another of just .cpp - # files to supply for library source files. Since we expect these - # file lists to only change due to GTAD invocation, we only use - # CONFIGURE_DEPENDS when pre-requisites to update API are met. - # Read comments next to each file(GLOB_RECURSE) for caveats. - set(add_CONFIGURE_DEPENDS "CONFIGURE_DEPENDS") - endif() + # We use globbing with CONFIGURE_DEPENDS to produce two file lists: + # one of all API files for clang-format and another of just .cpp + # files to supply for library source files. Since we expect these + # file lists to only change due to GTAD invocation, we only use + # CONFIGURE_DEPENDS when pre-requisites to update API are met. + # Read comments next to each file(GLOB_RECURSE) for caveats. + set(add_CONFIGURE_DEPENDS "CONFIGURE_DEPENDS") + set(FULL_CSAPI_SRC_DIR ${ABS_API_DEF_PATH}/client-server) file(GLOB_RECURSE API_DEFS RELATIVE ${PROJECT_SOURCE_DIR} ${FULL_CSAPI_SRC_DIR}/*.yaml @@ -280,14 +279,12 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} ${PROJECT_NAME}_VERSION_MINOR=${PROJECT_VERSION_MINOR} ${PROJECT_NAME}_VERSION_PATCH=${PROJECT_VERSION_PATCH} ${PROJECT_NAME}_VERSION_STRING=\"${PROJECT_VERSION}\") -if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" - AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 - target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) -endif () if (${PROJECT_NAME}_ENABLE_E2EE) target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_E2EE_ENABLED) endif() set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_EXTENSIONS OFF VERSION "${PROJECT_VERSION}" SOVERSION ${API_VERSION} INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${API_VERSION} @@ -295,8 +292,13 @@ set_target_properties(${PROJECT_NAME} PROPERTIES set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY COMPATIBLE_INTERFACE_STRING ${PROJECT_NAME}_MAJOR_VERSION) +# C++17 required, C++20 desired (see above) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) +if (NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 + target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) +endif () + target_include_directories(${PROJECT_NAME} PUBLIC $ $ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fdab602..c9296df1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -408,7 +408,7 @@ I (Kitsune) will be very glad to help you out. The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": * `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible. * `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around. -* `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h` - a substitute for `std::optional` from C++17 (we're still at C++14 yet). +* `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h`, a drop-in upgrade over `std::optional`. * `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). * `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. diff --git a/README.md b/README.md index 05c629f2..71f2d04c 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ The Quotient project aims to produce a Qt5-based SDK to develop applications for [Matrix](https://matrix.org). libQuotient is a library that enables client applications. It is the backbone of [Quaternion](https://github.com/quotient-im/Quaternion), -[Spectral](https://matrix.org/docs/projects/client/spectral.html) and -other projects. +[NeoChat](https://matrix.org/docs/projects/client/neo-chat) and other projects. Versions 0.5.x and older use the previous name - libQMatrixClient. ## Contacts @@ -38,61 +37,62 @@ and bundle it with your application. ### Pre-requisites - A recent Linux, macOS or Windows system (desktop versions are known to work; mobile operating systems where Qt is available might work too) - - Recent enough Linux examples: Debian Buster; Fedora 28; openSUSE Leap 15; - Ubuntu Bionic Beaver. -- Qt 5 (either Open Source or Commercial), 5.9 or higher; - 5.12 is recommended, especially if you use qmake -- A build configuration tool (CMake is recommended, qmake is supported): - - CMake 3.10 or newer (from your package management system or - [the official website](https://cmake.org/download/)) - - or qmake (comes with Qt) -- A C++ toolchain with _reasonably complete_ C++17 support: - - GCC 7 (Windows, Linux, macOS), Clang 6 (Linux), Apple Clang 10 (macOS) - and Visual Studio 2017 (Windows) are the oldest officially supported. -- Any build system that works with CMake and/or qmake should be fine: - GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. + - Recent enough Linux examples: Debian Bullseye; Fedora 33; openSUSE Leap 15.3; + Ubuntu Focal Fossa. +- Qt 5 (either Open Source or Commercial), 5.12 or higher +- CMake 3.16 or newer (from your package management system or + [the official website](https://cmake.org/download/)) +- A C++ toolchain with complete (as much as possible) C++17 and basic C++20: + - GCC 10 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) + and Visual Studio 2019 (Windows) are the oldest officially supported. +- Any build system that works with CMake should be fine: + GNU Make and ninja on any platform, NMake and jom on Windows are known to work. + Ninja is recommended. #### Linux -Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails. +Just install things from the list above using your preferred package manager. +If your Qt package base is fine-grained you might want to run cmake and look +at error messages. The library is entirely offscreen but aside from QtCore and +QtNetwork it also depends on QtGui in order to handle avatar thumbnails. #### macOS -`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you will need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` +`brew install qt5` should get you a recent Qt5. You may need to pass +`-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` to make it aware of the Qt location. #### Windows -Install Qt5, using their official installer; if you plan to build with CMake, -make sure to tick the CMake box in the list of installed components. - -The commands in further sections imply that cmake/qmake is in your PATH, -otherwise you have to prepend those commands with actual paths. As an option -it's a good idea to run a `qtenv2.bat` script that can be found in -`C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`); -the only thing it does is adding necessary paths to PATH. You might not want -to run that script on system startup but it's very handy to setup -the environment before building. For CMake you can alternatively point -`CMAKE_PREFIX_PATH` to your Qt installation and leave PATH unchanged; but -in that case you'll have to supply the full path to CMake when calling it. +Install Qt5 using their official installer; make sure to tick the CMake box +in the list of installed components unless you already have it installed. + +The commands in further sections imply that cmake is in your PATH, otherwise +you have to prepend those commands with actual paths. It's a good idea to run +a `qtenv2.bat` script that can be found in `C:\Qt\\\bin` +(assuming you installed Qt to `C:\Qt`) if you're building from the command line; +the script adds necessary paths to PATH. You might not want to run that script +on system startup but it's very handy to setup the environment before building. +Alternatively you can add the Qt path to `CMAKE_PREFIX_PATH` and leave PATH +unchanged. ### Using the library -If you use CMake, `find_package(Quotient)` sets up the client code to use -libQuotient, assuming the library development files are installed. There's no -documented procedure to use a preinstalled library with qmake; consider -introducing a submodule in your source tree and build it along with the rest -of the application for now. Note also that qmake is considered for phase-out -in Qt 6 so you should probably think of moving over to CMake eventually. +If you're just starting a project using libQuotient from scratch, you can copy +`quotest/CMakeLists.txt` to your project and change `quotest` to your +project name. If you already have an existing CMakeLists.txt, you need to insert +a `find_package(Quotient REQUIRED)` line to an appropriate place in it (use +`find_package(Quotient)` if libQuotient is not a hard dependency for you) and +then add `Quotient` to your `target_link_libraries()` line. Building with dynamic linkage is only tested on Linux at the moment and is a recommended way of linking your application with libQuotient on this platform. Static linkage is the default on Windows/macOS; feel free to experiment with dynamic linking and submit PRs if you get reusable results. -[Quotest](quotest), the test application that comes with libQuotient, includes -most common use cases such as sending messages, uploading files, -setting room state etc.; for more extensive usage check out the source code -of [Quaternion](https://github.com/quotient-im/Quaternion) -(the reference client of Quotient) or [Spectral](https://gitlab.com/b0/spectral). - -To ease the first step, `quotest/CMakeLists.txt` is a good starting point -for your own CMake-based project using libQuotient. +As for the actual API usage, a (very basic) overview can be found at +[the respective wiki page](https://github.com/quotient-im/libQuotient/wiki/libQuotient-overview). +Beyond that, looking at [Quotest](quotest) - the test application that comes +with libQuotient - may help you with most common use cases such as sending +messages, uploading files, setting room state etc. For more extensive usage +feel free to check out (and copy, with appropriate attribution) the source code +of [Quaternion](https://github.com/quotient-im/Quaternion) (the reference client +for libQuotient) or [NeoChat](https://invent.kde.org/network/neochat). ## Building the library [The source code is at GitHub](https://github.com/quotient-im/libQuotient). @@ -101,28 +101,29 @@ along with submodules is strongly recommended. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), it makes sense to make a recursive check out of that project (in this case, Quaternion) -and update the library submodule (also recursively) to its master branch. +and update the library submodule (also recursively) within the appropriate +branch. Be mindful of API compatibility restrictions: e.g., Quaternion 0.0.95 +will not build with the master branch of libQuotient. Tags consisting of digits and periods represent released versions; tags ending with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, it is advised to replace a dash with a tilde. -### CMake-based -In the root directory of the project sources: +The following commands issued in the root directory of the project sources: ```shell script mkdir build_dir cd build_dir cmake .. # [-D=...], see below cmake --build . --target all ``` -This will get you the compiled library in `build_dir` inside your project -sources. Static builds are tested on all supported platforms, building -the library as a shared object (aka dynamic library) is supported on Linux -and macOS but is very likely to be broken on Windows. - -The first CMake invocation configures the build. You can pass CMake variables, -such as `-DCMAKE_PREFIX_PATH="path1;path2;..."` and -`-DCMAKE_INSTALL_PREFIX=path` here if needed. +will get you a compiled library in `build_dir` inside your project sources. +Static builds are tested on all supported platforms, building the library as +a shared object (aka dynamic library) is supported on Linux and macOS but is +very likely to be broken on Windows. + +The first CMake invocation above configures the build. You can pass CMake +variables (such as `-DCMAKE_PREFIX_PATH="path1;path2;..."` and +`-DCMAKE_INSTALL_PREFIX=path`) here if needed. [CMake documentation](https://cmake.org/cmake/help/latest/index.html) (pick the CMake version at the top of the page that you use) describes the standard variables coming with CMake. On top of them, Quotient introduces: @@ -160,22 +161,6 @@ with the _installed_ library. Installation of the `quotest` binary along with the rest of the library can be skipped by setting `Quotient_INSTALL_TESTS` to `OFF`. -### qmake-based -The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `quotest.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: -```shell script -qmake quotest.pro -make all -``` -This will get you `debug/quotest` and `release/quotest` -console executables that login to the Matrix server at matrix.org with -credentials of your choosing (pass the username and password as arguments), -run a sync long-polling loop and do some tests of the library API. Note that -qmake didn't really know about C++17 until Qt 5.12 so if your Qt is older -you may have quite a bit of warnings during the compilation process. - -Installing the standalone library with qmake is not implemented yet; PRs are -welcome though. - ## Troubleshooting #### Building fails -- cgit v1.2.3 From ee678fac0ee09c6262c7a39b2b1072660e6db9ca Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 2 Sep 2021 22:18:14 +0200 Subject: CI experiment: requires GCC 10 and Clang 11 --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 670153eb..47e31d55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,16 +72,17 @@ jobs: - name: Setup build environment run: | if [ "${{ matrix.compiler }}" == "GCC" ]; then - if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-9'; fi - echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV - echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV + CXX_VERSION_POSTFIX='-10' + echo "CC=gcc$CXX_VERSION_POSTFIX" >>$GITHUB_ENV + echo "CXX=g++$CXX_VERSION_POSTFIX" >>$GITHUB_ENV elif [[ '${{ matrix.compiler }}' == 'Clang' ]]; then - echo "CC=clang" >>$GITHUB_ENV - echo "CXX=clang++" >>$GITHUB_ENV if [[ '${{ runner.os }}' == 'Linux' ]]; then + CXX_VERSION_POSTFIX='-11' # Do CodeQL analysis on one of Linux branches echo "CODEQL_ANALYSIS=true" >>$GITHUB_ENV fi + echo "CC=clang$CXX_VERSION_POSTFIX" >>$GITHUB_ENV + echo "CXX=clang++$CXX_VERSION_POSTFIX" >>$GITHUB_ENV fi if grep -q 'refs/tags' <<<'${{ github.ref }}'; then VERSION="$(git describe --tags)" -- cgit v1.2.3 From 2736eb8ff2e95daf6ec8b2616d70381054d1a238 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 3 Sep 2021 06:54:29 +0200 Subject: CONTRIBUTING.md: update code conventions to C++20 --- CONTRIBUTING.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9296df1..3eeac68c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -155,18 +155,20 @@ just don't bankrupt us with it. Refactoring is welcome. ### Code style and formatting -As of Quotient 0.6, the C++ standard for newly written code is C++17, with a few -restrictions, notably: +As of Quotient 0.6, the C++ standard for newly written code is C++17 with C++20 +compatibility and a few restrictions, notably: * standard library's _deduction guides_ cannot be used to lighten up syntax in template instantiation, i.e. you have to still write - `std::array { 1, 2 }` instead of `std::array { 1, 2 }` or use helpers - like `std::make_pair` - once we move over to the later Apple toolchain, this - will be no more necessary. -* enumerators and slots cannot have `[[attributes]]` because moc of Qt 5.9 - chokes on them. This will be lifted when we move on to Qt 5.12 for the oldest - supported version. -* things from `std::filesystem` cannot be used until we push the oldest - required g++/libc to version 8. + `std::array { 1, 2 }` instead of `std::array { 1, 2 }` (or use + `Quotient::make_array` helper from `util.h`), use `std::make_pair` to create + pairs etc. - once we move over to the later Apple toolchain, this will be + no more necessary; +* enumerators and slots cannot have `[[attributes]]` because moc from Qt 5.12 + chokes on them - this will be lifted when we move on to Qt 5.13 for the oldest + supported version, in the meantime use `Q_DECL_DEPRECATED` and similar Qt + macros - they expand to nothing when the code is passed to moc. +* explicit lists in lambda captures are preferred over `[=]`; note that C++20 + deprecates implicit `this` capture in `[=]`. The code style is defined by `.clang-format`, and in general, all C++ files should follow it. Files with minor deviations from the defined style are still -- cgit v1.2.3 From df66314bb47f1a824f35e9d5c7764305a4f103cb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 3 Sep 2021 07:20:27 +0200 Subject: Tacitly allow CMake 3.13 to keep LGTM working Also: drop olm from the LGTM build environment, it's of no use there for now. --- .lgtm.yml | 18 ++++++++---------- CMakeLists.txt | 9 +++++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.lgtm.yml b/.lgtm.yml index f6dfb229..308675a8 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -6,15 +6,13 @@ path_classifiers: extraction: cpp: prepare: - packages: # Assuming package base of cosmic - - ninja-build - - qt5-default + packages: # Assuming package base of eoan - qtmultimedia5-dev - after_prepare: - - git clone https://gitlab.matrix.org/matrix-org/olm.git - - pushd olm - - cmake . -Bbuild -GNinja - - cmake --build build - - popd +# after_prepare: +# - git clone https://gitlab.matrix.org/matrix-org/olm.git +# - pushd olm +# - cmake . -Bbuild -GNinja +# - cmake --build build +# - popd configure: - command: "cmake . -GNinja -DOlm_DIR=olm/build" + command: "cmake . -GNinja" # -DOlm_DIR=olm/build" diff --git a/CMakeLists.txt b/CMakeLists.txt index c0a39932..7043a653 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,6 @@ -cmake_minimum_required(VERSION 3.16) +# Officially CMake 3.16+ is needed but LGTM.com still sits on eoan that only +# has CMake 3.13 +cmake_minimum_required(VERSION 3.13) if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() @@ -295,7 +297,10 @@ set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY # C++17 required, C++20 desired (see above) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) -if (NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 +# TODO: Bump the CMake requirement and drop the version check here once +# LGTM upgrades +if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" + AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) endif () -- cgit v1.2.3 From 4bab0f2ef2c68b478d669f90557d6bef6332e823 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 21:47:10 +0200 Subject: Implement the mxc protocol in the NetworkAccessManager Allows images to be loaded using the NetworkAccessManager instead of an ImageProvider --- CMakeLists.txt | 1 + lib/mxcreply.cpp | 54 ++++++++++++++++++++++++++++++++++++++++++ lib/mxcreply.h | 29 +++++++++++++++++++++++ lib/networkaccessmanager.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++ lib/networkaccessmanager.h | 6 +++++ 5 files changed, 146 insertions(+) create mode 100644 lib/mxcreply.cpp create mode 100644 lib/mxcreply.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c6e7548..178602da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,7 @@ list(APPEND lib_SRCS lib/encryptionmanager.cpp lib/eventitem.cpp lib/accountregistry.cpp + lib/mxcreply.cpp lib/events/event.cpp lib/events/roomevent.cpp lib/events/stateevent.cpp diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp new file mode 100644 index 00000000..49ebe603 --- /dev/null +++ b/lib/mxcreply.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "mxcreply.h" + +#include +#include "connection.h" +#include "room.h" +#include "networkaccessmanager.h" +#include "events/stickerevent.h" + +using namespace Quotient; + +class MxcReply::Private +{ +public: + QNetworkReply *m_reply = nullptr; +}; + +MxcReply::MxcReply(QNetworkReply* reply) +{ + reply->setParent(this); + d->m_reply = reply; + connect(d->m_reply, &QNetworkReply::finished, this, [this]() { + setError(d->m_reply->error(), d->m_reply->errorString()); + Q_EMIT finished(); + }); +} + +MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) + : d(std::make_unique()) +{ + reply->setParent(this); + d->m_reply = reply; + connect(d->m_reply, &QNetworkReply::finished, this, [this, eventId]() { + setError(d->m_reply->error(), d->m_reply->errorString()); + Q_EMIT finished(); + }); +} + +bool MxcReply::isSequential() const +{ + return true; +} + +qint64 MxcReply::readData(char *data, qint64 maxSize) +{ + return d->m_reply->read(data, maxSize); +} + +void MxcReply::abort() +{ + d->m_reply->abort(); +} \ No newline at end of file diff --git a/lib/mxcreply.h b/lib/mxcreply.h new file mode 100644 index 00000000..26dea2d0 --- /dev/null +++ b/lib/mxcreply.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +namespace Quotient { +class Room; +class Connection; +class MxcReply : public QNetworkReply +{ +public: + MxcReply(QNetworkReply* reply, Room* room, const QString &eventId); + MxcReply(QNetworkReply* reply); + + bool isSequential() const override; + +public slots: + void abort() override; + +protected: + qint64 readData(char *data, qint64 maxSize) override; +private: + class Private; + std::unique_ptr d; +}; +} \ No newline at end of file diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index a94ead34..e4132957 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -5,6 +5,11 @@ #include #include +#include "accountregistry.h" +#include "mxcreply.h" +#include "connection.h" + +#include "room.h" using namespace Quotient; @@ -56,7 +61,58 @@ NetworkAccessManager::~NetworkAccessManager() = default; QNetworkReply* NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { + if(request.url().scheme() == QStringLiteral("mxc")) { + const auto fragment = request.url().fragment(); + const auto fragmentParts = fragment.split(QLatin1Char('/')); + const auto mediaId = request.url().toString(QUrl::RemoveScheme | QUrl::RemoveFragment); + if(fragmentParts.size() == 3) { + auto connection = AccountRegistry::instance().get(fragmentParts[0]); + if(!connection) { + qWarning() << "Connection not found"; + return nullptr; + } + auto room = connection->room(fragmentParts[1]); + if(!room) { + qWarning() << "Room not found"; + return nullptr; + } + QNetworkRequest r(request); + r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); + auto reply = createRequest(QNetworkAccessManager::GetOperation, r); + return new MxcReply(reply, room, fragmentParts[2]); + } else if(fragmentParts.size() == 1) { + auto connection = AccountRegistry::instance().get(fragment); + if(!connection) { + qWarning() << "Connection not found"; + return nullptr; + } + QNetworkRequest r(request); + r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); + auto reply = createRequest(QNetworkAccessManager::GetOperation, r); + return new MxcReply(reply); + } else { + qWarning() << "Invalid request"; + return nullptr; + } + } auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData); reply->ignoreSslErrors(d->ignoredSslErrors); return reply; } + +QStringList NetworkAccessManager::supportedSchemesImplementation() const +{ + auto schemes = QNetworkAccessManager::supportedSchemesImplementation(); + schemes += QStringLiteral("mxc"); + return schemes; +} + +QUrl NetworkAccessManager::urlForRoomEvent(Room *room, const QString &eventId, const QString &mediaId) +{ + return QUrl(QStringLiteral("mxc:%1#%2/%3/%4").arg(mediaId, room->connection()->userId(), room->id(), eventId)); +} + +QUrl NetworkAccessManager::urlForFile(Connection *connection, const QString &mediaId) +{ + return QUrl(QStringLiteral("mxc:%1#%2").arg(mediaId, connection->userId())); +} \ No newline at end of file diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 47729a1b..5d262f98 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -8,6 +8,8 @@ #include namespace Quotient { +class Room; +class Connection; class NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: @@ -21,6 +23,10 @@ public: /** Get a pointer to the singleton */ static NetworkAccessManager* instance(); +public Q_SLOTS: + QStringList supportedSchemesImplementation() const; + QUrl urlForRoomEvent(Room *room, const QString &eventId, const QString &mediaId); + QUrl urlForFile(Connection *connection, const QString &mediaId); private: QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* outgoingData = Q_NULLPTR) override; -- cgit v1.2.3 From 67186252eac4f3890d5abae31e74efee956b2f5d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 5 Sep 2021 22:52:41 +0200 Subject: Create a NAM for each thread --- lib/networkaccessmanager.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index e4132957..7368de7e 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "accountregistry.h" #include "mxcreply.h" #include "connection.h" @@ -52,8 +53,11 @@ static NetworkAccessManager* createNam() NetworkAccessManager* NetworkAccessManager::instance() { - static auto* nam = createNam(); - return nam; + static QThreadStorage storage; + if(!storage.hasLocalData()) { + storage.setLocalData(createNam()); + } + return storage.localData(); } NetworkAccessManager::~NetworkAccessManager() = default; -- cgit v1.2.3 From 6d3bc1667e084fa2fc7b2c547374d2c62a29e8df Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Sep 2021 16:17:23 +0200 Subject: Fix showing non-animated Images --- lib/mxcreply.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 49ebe603..f389ac85 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -23,6 +23,7 @@ MxcReply::MxcReply(QNetworkReply* reply) d->m_reply = reply; connect(d->m_reply, &QNetworkReply::finished, this, [this]() { setError(d->m_reply->error(), d->m_reply->errorString()); + setOpenMode(ReadOnly); Q_EMIT finished(); }); } @@ -34,6 +35,7 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) d->m_reply = reply; connect(d->m_reply, &QNetworkReply::finished, this, [this, eventId]() { setError(d->m_reply->error(), d->m_reply->errorString()); + setOpenMode(ReadOnly); Q_EMIT finished(); }); } -- 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 ++++++++++++------ lib/room.h | 3 ++- 2 files changed, 14 insertions(+), 7 deletions(-) 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 diff --git a/lib/room.h b/lib/room.h index d3a7466d..4e55dbaf 100644 --- a/lib/room.h +++ b/lib/room.h @@ -712,7 +712,8 @@ Q_SIGNALS: void fileTransferProgress(QString id, qint64 progress, qint64 total); void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); void fileTransferFailed(QString id, QString errorMessage = {}); - void fileTransferCancelled(QString id); + // fileTransferCancelled() is no more here; use fileTransferFailed() and + // check the transfer status instead void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event); -- cgit v1.2.3 From c1015d78fc90972a87b02a7863a9148c446c94b1 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Fri, 10 Sep 2021 17:36:11 +0200 Subject: Update lib/networkaccessmanager.cpp Co-authored-by: Alexey Rusakov --- lib/networkaccessmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 7368de7e..9f423cc3 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -69,7 +69,7 @@ QNetworkReply* NetworkAccessManager::createRequest( const auto fragment = request.url().fragment(); const auto fragmentParts = fragment.split(QLatin1Char('/')); const auto mediaId = request.url().toString(QUrl::RemoveScheme | QUrl::RemoveFragment); - if(fragmentParts.size() == 3) { + if (fragmentParts.size() == 3) { auto connection = AccountRegistry::instance().get(fragmentParts[0]); if(!connection) { qWarning() << "Connection not found"; -- cgit v1.2.3 From 3b383a6dcb75531ca7efcaa4afa28b92dbe15e3e Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Fri, 10 Sep 2021 17:36:19 +0200 Subject: Update lib/networkaccessmanager.cpp Co-authored-by: Alexey Rusakov --- lib/networkaccessmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 9f423cc3..710ade4e 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -53,7 +53,7 @@ static NetworkAccessManager* createNam() NetworkAccessManager* NetworkAccessManager::instance() { - static QThreadStorage storage; + static QThreadStorage storage; if(!storage.hasLocalData()) { storage.setLocalData(createNam()); } -- cgit v1.2.3 From 8dfa505066a03cc8450527699634fda71cbd8915 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 10 Sep 2021 17:55:20 +0200 Subject: Return a failed MxcReply on invalid requests --- lib/mxcreply.cpp | 15 +++++++++++++++ lib/mxcreply.h | 1 + lib/networkaccessmanager.cpp | 8 ++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index f389ac85..7819367e 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -4,6 +4,7 @@ #include "mxcreply.h" #include +#include #include "connection.h" #include "room.h" #include "networkaccessmanager.h" @@ -40,6 +41,20 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) }); } +MxcReply::MxcReply() +{ + QTimer::singleShot(0, this, [this](){ + setError(QNetworkReply::ProtocolInvalidOperationError, QStringLiteral("Invalid Request")); + setFinished(true); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + Q_EMIT errorOccurred(QNetworkReply::ProtocolInvalidOperationError); +#else + Q_EMIT error(QNetworkReply::ProtocolInvalidOperationError); +#endif + Q_EMIT finished(); + }); +} + bool MxcReply::isSequential() const { return true; diff --git a/lib/mxcreply.h b/lib/mxcreply.h index 26dea2d0..ac3ac4f4 100644 --- a/lib/mxcreply.h +++ b/lib/mxcreply.h @@ -14,6 +14,7 @@ class MxcReply : public QNetworkReply public: MxcReply(QNetworkReply* reply, Room* room, const QString &eventId); MxcReply(QNetworkReply* reply); + MxcReply(); bool isSequential() const override; diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 710ade4e..f37e26b6 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -73,12 +73,12 @@ QNetworkReply* NetworkAccessManager::createRequest( auto connection = AccountRegistry::instance().get(fragmentParts[0]); if(!connection) { qWarning() << "Connection not found"; - return nullptr; + return new MxcReply(); } auto room = connection->room(fragmentParts[1]); if(!room) { qWarning() << "Room not found"; - return nullptr; + return new MxcReply(); } QNetworkRequest r(request); r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); @@ -88,7 +88,7 @@ QNetworkReply* NetworkAccessManager::createRequest( auto connection = AccountRegistry::instance().get(fragment); if(!connection) { qWarning() << "Connection not found"; - return nullptr; + return new MxcReply(); } QNetworkRequest r(request); r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); @@ -96,7 +96,7 @@ QNetworkReply* NetworkAccessManager::createRequest( return new MxcReply(reply); } else { qWarning() << "Invalid request"; - return nullptr; + return new MxcReply(); } } auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData); -- cgit v1.2.3 From 4106a073d03b4ba9176da24c6b169c5fc2c79fb4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 10 Sep 2021 22:10:16 +0200 Subject: Percent-encode all the things --- lib/networkaccessmanager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index f37e26b6..dc1c139c 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -70,7 +70,7 @@ QNetworkReply* NetworkAccessManager::createRequest( const auto fragmentParts = fragment.split(QLatin1Char('/')); const auto mediaId = request.url().toString(QUrl::RemoveScheme | QUrl::RemoveFragment); if (fragmentParts.size() == 3) { - auto connection = AccountRegistry::instance().get(fragmentParts[0]); + auto connection = AccountRegistry::instance().get(QUrl::fromPercentEncoding(fragmentParts[0].toLatin1())); if(!connection) { qWarning() << "Connection not found"; return new MxcReply(); @@ -83,7 +83,7 @@ QNetworkReply* NetworkAccessManager::createRequest( QNetworkRequest r(request); r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); auto reply = createRequest(QNetworkAccessManager::GetOperation, r); - return new MxcReply(reply, room, fragmentParts[2]); + return new MxcReply(reply, room, QUrl::fromPercentEncoding(fragmentParts[2].toLatin1())); } else if(fragmentParts.size() == 1) { auto connection = AccountRegistry::instance().get(fragment); if(!connection) { @@ -113,10 +113,10 @@ QStringList NetworkAccessManager::supportedSchemesImplementation() const QUrl NetworkAccessManager::urlForRoomEvent(Room *room, const QString &eventId, const QString &mediaId) { - return QUrl(QStringLiteral("mxc:%1#%2/%3/%4").arg(mediaId, room->connection()->userId(), room->id(), eventId)); + return QUrl(QStringLiteral("mxc:%1#%2/%3/%4").arg(mediaId, QString(QUrl::toPercentEncoding(room->connection()->userId())), room->id(), QString(QUrl::toPercentEncoding(eventId)))); } QUrl NetworkAccessManager::urlForFile(Connection *connection, const QString &mediaId) { - return QUrl(QStringLiteral("mxc:%1#%2").arg(mediaId, connection->userId())); + return QUrl(QStringLiteral("mxc:%1#%2").arg(mediaId, QString(QUrl::toPercentEncoding(connection->userId())))); } \ No newline at end of file -- cgit v1.2.3 From df46414a4e16d608049610935aeabab222e06d72 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 10 Sep 2021 21:53:22 +0200 Subject: Add "quotient.network" logging category --- lib/logging.cpp | 1 + lib/logging.h | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/logging.cpp b/lib/logging.cpp index ffcc851c..15eac69d 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -17,4 +17,5 @@ LOGGING_CATEGORY(E2EE, "quotient.e2ee") LOGGING_CATEGORY(JOBS, "quotient.jobs") LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") +LOGGING_CATEGORY(NETWORK, "quotient.network") LOGGING_CATEGORY(PROFILER, "quotient.profiler") diff --git a/lib/logging.h b/lib/logging.h index 1d1394e8..7e0da975 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -17,6 +17,7 @@ Q_DECLARE_LOGGING_CATEGORY(E2EE) Q_DECLARE_LOGGING_CATEGORY(JOBS) Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB) +Q_DECLARE_LOGGING_CATEGORY(NETWORK) Q_DECLARE_LOGGING_CATEGORY(PROFILER) namespace Quotient { -- 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/connection.cpp | 9 ++++ lib/connection.h | 2 + lib/networkaccessmanager.cpp | 97 ++++++++++++++++++++++++-------------------- lib/networkaccessmanager.h | 3 +- lib/room.cpp | 11 +++++ lib/room.h | 3 ++ 6 files changed, 80 insertions(+), 45 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 222c3b71..51946b2f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -836,6 +836,15 @@ inline auto splitMediaId(const QString& mediaId) return idParts; } +QUrl Connection::makeMediaUrl(QUrl mxcUrl) const +{ + Q_ASSERT(mxcUrl.scheme() == "mxc"); + QUrlQuery q(mxcUrl.query()); + q.addQueryItem(QStringLiteral("user_id"), userId()); + mxcUrl.setQuery(q); + return mxcUrl; +} + MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy) diff --git a/lib/connection.h b/lib/connection.h index ecbb1a19..1a6ca9b0 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -529,6 +529,8 @@ public Q_SLOTS: void stopSync(); QString nextBatchToken() const; + Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const; + virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest); diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index dc1c139c..3b0dc92b 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -3,24 +3,43 @@ #include "networkaccessmanager.h" -#include -#include -#include +#include "connection.h" +#include "room.h" #include "accountregistry.h" #include "mxcreply.h" -#include "connection.h" -#include "room.h" +#include +#include +#include +#include using namespace Quotient; class NetworkAccessManager::Private { public: + explicit Private(NetworkAccessManager* q) + : q(q) + {} + + QNetworkReply* createImplRequest(Operation op, + const QNetworkRequest& outerRequest, + Connection* connection) + { + Q_ASSERT(outerRequest.url().scheme() == "mxc"); + QNetworkRequest r(outerRequest); + r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2") + .arg(connection->homeserver().toString(), + outerRequest.url().authority() + + outerRequest.url().path()))); + return q->createRequest(op, r); + } + + NetworkAccessManager* q; QList ignoredSslErrors; }; NetworkAccessManager::NetworkAccessManager(QObject* parent) - : QNetworkAccessManager(parent), d(std::make_unique()) + : QNetworkAccessManager(parent), d(std::make_unique(this)) {} QList NetworkAccessManager::ignoredSslErrors() const @@ -54,6 +73,8 @@ static NetworkAccessManager* createNam() NetworkAccessManager* NetworkAccessManager::instance() { static QThreadStorage storage; + // FIXME: createNam() returns an object parented to + // QCoreApplication::instance() that lives in the main thread if(!storage.hasLocalData()) { storage.setLocalData(createNam()); } @@ -65,38 +86,38 @@ NetworkAccessManager::~NetworkAccessManager() = default; QNetworkReply* NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { - if(request.url().scheme() == QStringLiteral("mxc")) { - const auto fragment = request.url().fragment(); - const auto fragmentParts = fragment.split(QLatin1Char('/')); - const auto mediaId = request.url().toString(QUrl::RemoveScheme | QUrl::RemoveFragment); - if (fragmentParts.size() == 3) { - auto connection = AccountRegistry::instance().get(QUrl::fromPercentEncoding(fragmentParts[0].toLatin1())); - if(!connection) { - qWarning() << "Connection not found"; + const auto& mxcUrl = request.url(); + if (mxcUrl.scheme() == "mxc") { + const QUrlQuery query(mxcUrl.query()); + const auto accountId = query.queryItemValue(QStringLiteral("user_id")); + if (accountId.isEmpty()) { + // Using QSettings here because Quotient::NetworkSettings + // doesn't provide multithreading guarantees + static thread_local QSettings s; + if (!s.value("Network/allow_direct_media_requests").toBool()) { return new MxcReply(); } - auto room = connection->room(fragmentParts[1]); - if(!room) { - qWarning() << "Room not found"; + // TODO: Make the best effort with a direct unauthenticated request + // to the media server + } else { + auto* const connection = AccountRegistry::instance().get(accountId); + if (!connection) { + qCWarning(NETWORK) << "Connection not found"; return new MxcReply(); } - QNetworkRequest r(request); - r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); - auto reply = createRequest(QNetworkAccessManager::GetOperation, r); - return new MxcReply(reply, room, QUrl::fromPercentEncoding(fragmentParts[2].toLatin1())); - } else if(fragmentParts.size() == 1) { - auto connection = AccountRegistry::instance().get(fragment); - if(!connection) { - qWarning() << "Connection not found"; - return new MxcReply(); + const auto roomId = query.queryItemValue(QStringLiteral("room_id")); + if (!roomId.isEmpty()) { + auto room = connection->room(roomId); + if (!room) { + qCWarning(NETWORK) << "Room not found"; + return new MxcReply(); + } + return new MxcReply( + d->createImplRequest(op, request, connection), room, + query.queryItemValue(QStringLiteral("event_id"))); } - QNetworkRequest r(request); - r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2").arg(connection->homeserver().toString(), mediaId))); - auto reply = createRequest(QNetworkAccessManager::GetOperation, r); - return new MxcReply(reply); - } else { - qWarning() << "Invalid request"; - return new MxcReply(); + return new MxcReply( + d->createImplRequest(op, request, connection)); } } auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData); @@ -110,13 +131,3 @@ QStringList NetworkAccessManager::supportedSchemesImplementation() const schemes += QStringLiteral("mxc"); return schemes; } - -QUrl NetworkAccessManager::urlForRoomEvent(Room *room, const QString &eventId, const QString &mediaId) -{ - return QUrl(QStringLiteral("mxc:%1#%2/%3/%4").arg(mediaId, QString(QUrl::toPercentEncoding(room->connection()->userId())), room->id(), QString(QUrl::toPercentEncoding(eventId)))); -} - -QUrl NetworkAccessManager::urlForFile(Connection *connection, const QString &mediaId) -{ - return QUrl(QStringLiteral("mxc:%1#%2").arg(mediaId, QString(QUrl::toPercentEncoding(connection->userId())))); -} \ No newline at end of file diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 5d262f98..87bc12a1 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -25,8 +25,7 @@ public: public Q_SLOTS: QStringList supportedSchemesImplementation() const; - QUrl urlForRoomEvent(Room *room, const QString &eventId, const QString &mediaId); - QUrl urlForFile(Connection *connection, const QString &mediaId); + private: QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* outgoingData = Q_NULLPTR) override; 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("[/\\<>|\"*?:]"), "_"); diff --git a/lib/room.h b/lib/room.h index d3a7466d..9daca076 100644 --- a/lib/room.h +++ b/lib/room.h @@ -458,6 +458,9 @@ public: /// Get the list of users this room is a direct chat with QList directChatUsers() const; + Q_INVOKABLE QUrl makeMediaUrl(const QString& eventId, + const QUrl &mxcUrl) const; + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; -- cgit v1.2.3 From 4babd9b2f1ba1d8c8c58c2f728cc4875ecf144c7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Sep 2021 11:14:04 +0200 Subject: Don't parent NAM to QCoreApplication QThreadStorage accepts ownership over stored objects. --- lib/networkaccessmanager.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 3b0dc92b..d35b2ec8 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -59,7 +59,7 @@ void NetworkAccessManager::clearIgnoredSslErrors() static NetworkAccessManager* createNam() { - auto nam = new NetworkAccessManager(QCoreApplication::instance()); + auto nam = new NetworkAccessManager(); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) // See #109; in newer Qt, bearer management is deprecated altogether NetworkAccessManager::connect(nam, @@ -73,8 +73,6 @@ static NetworkAccessManager* createNam() NetworkAccessManager* NetworkAccessManager::instance() { static QThreadStorage storage; - // FIXME: createNam() returns an object parented to - // QCoreApplication::instance() that lives in the main thread if(!storage.hasLocalData()) { storage.setLocalData(createNam()); } -- cgit v1.2.3 From 6597866ead7a3eb03cfcbbd99b547de1bb72867e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Sep 2021 13:05:40 +0200 Subject: Further tweaks to MxcReply - QNetworkReply::isSequential() already returns `true`, there's no need to overload it again. - Use `Q_SLOTS` instead of `slots` because it's an external library interface and clients may use other libraries using `slots` identifier; - Use `emit` instead of `Q_EMIT` because this is a part of internal implementation and if we ever use a library that has an `emit` identifier, a massive search-replace will be in order anyway. - Use `QMetaObject::invokeMethod()` with a queued connection as a clearer way to achieve the same goal as `QTimer::singleShot(0, ...)`. --- lib/mxcreply.cpp | 37 +++++++++++++++++-------------------- lib/mxcreply.h | 15 +++++++-------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 7819367e..daa4af9a 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -3,12 +3,7 @@ #include "mxcreply.h" -#include -#include -#include "connection.h" #include "room.h" -#include "networkaccessmanager.h" -#include "events/stickerevent.h" using namespace Quotient; @@ -34,30 +29,32 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) { reply->setParent(this); d->m_reply = reply; - connect(d->m_reply, &QNetworkReply::finished, this, [this, eventId]() { + connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() { setError(d->m_reply->error(), d->m_reply->errorString()); setOpenMode(ReadOnly); - Q_EMIT finished(); + emit finished(); }); } -MxcReply::MxcReply() -{ - QTimer::singleShot(0, this, [this](){ - setError(QNetworkReply::ProtocolInvalidOperationError, QStringLiteral("Invalid Request")); - setFinished(true); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - Q_EMIT errorOccurred(QNetworkReply::ProtocolInvalidOperationError); +#define ERROR_SIGNAL errorOccurred #else - Q_EMIT error(QNetworkReply::ProtocolInvalidOperationError); +#define ERROR_SIGNAL error #endif - Q_EMIT finished(); - }); -} -bool MxcReply::isSequential() const +MxcReply::MxcReply() { - return true; + static const auto BadRequestPhrase = tr("Bad Request"); + QMetaObject::invokeMethod(this, [this]() { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 400); + setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, + BadRequestPhrase); + setError(QNetworkReply::ProtocolInvalidOperationError, + BadRequestPhrase); + setFinished(true); + emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError); + emit finished(); + }, Qt::QueuedConnection); } qint64 MxcReply::readData(char *data, qint64 maxSize) @@ -68,4 +65,4 @@ qint64 MxcReply::readData(char *data, qint64 maxSize) void MxcReply::abort() { d->m_reply->abort(); -} \ No newline at end of file +} diff --git a/lib/mxcreply.h b/lib/mxcreply.h index ac3ac4f4..efaf01c6 100644 --- a/lib/mxcreply.h +++ b/lib/mxcreply.h @@ -8,23 +8,22 @@ namespace Quotient { class Room; -class Connection; + class MxcReply : public QNetworkReply { public: - MxcReply(QNetworkReply* reply, Room* room, const QString &eventId); - MxcReply(QNetworkReply* reply); - MxcReply(); - - bool isSequential() const override; + explicit MxcReply(); + explicit MxcReply(QNetworkReply *reply); + MxcReply(QNetworkReply* reply, Room* room, const QString& eventId); -public slots: +public Q_SLOTS: void abort() override; protected: qint64 readData(char *data, qint64 maxSize) override; + private: class Private; std::unique_ptr d; }; -} \ No newline at end of file +} -- cgit v1.2.3 From 9da6b25a26403952e5a76b043076ba302c8d3c30 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Sep 2021 20:35:00 +0200 Subject: BaseJob: deprecate endpoint accessors; query returns an object To provide more room for internal changes in BaseJob. --- lib/jobs/basejob.cpp | 24 ++++++++++-------------- lib/jobs/basejob.h | 4 +++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 6346db9d..85066024 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -194,12 +194,12 @@ QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); } bool BaseJob::isBackground() const { return d->inBackground; } -const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; } +//const QString& BaseJob::apiEndpoint() const { return d->apiUrl.path(); } -void BaseJob::setApiEndpoint(const QString& apiEndpoint) -{ - d->apiEndpoint = apiEndpoint; -} +//void BaseJob::setApiEndpoint(const QString& apiEndpoint) +//{ +// d->apiEndpoint = apiEndpoint; +//} const BaseJob::headers_t& BaseJob::requestHeaders() const { @@ -217,7 +217,7 @@ void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers) d->requestHeaders = headers; } -const QUrlQuery& BaseJob::query() const { return d->requestQuery; } +QUrlQuery BaseJob::query() const { return d->requestQuery; } void BaseJob::setRequestQuery(const QUrlQuery& query) { @@ -262,14 +262,10 @@ QNetworkReply* BaseJob::reply() { return d->reply.data(); } QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path, const QUrlQuery& query) { - auto pathBase = baseUrl.path(); - // QUrl::adjusted(QUrl::StripTrailingSlashes) doesn't help with root '/' - while (pathBase.endsWith('/')) - pathBase.chop(1); - if (!path.startsWith('/')) // Normally API files do start with '/' - pathBase.push_back('/'); // so this shouldn't be needed these days - - baseUrl.setPath(pathBase + path, QUrl::TolerantMode); + // Make sure the added path is relative even if it's not (the official + // API definitions have the leading slash though it's not really correct). + baseUrl = baseUrl.resolved( + QUrl(path.mid(path.startsWith('/')), QUrl::TolerantMode)); baseUrl.setQuery(query); return baseUrl; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 29443c53..663c121c 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -325,13 +325,15 @@ Q_SIGNALS: protected: using headers_t = QHash; + Q_DECL_DEPRECATED_X("Deprecated due to being unused") const QString& apiEndpoint() const; + Q_DECL_DEPRECATED_X("Deprecated due to being unused") void setApiEndpoint(const QString& apiEndpoint); const headers_t& requestHeaders() const; void setRequestHeader(const headers_t::key_type& headerName, const headers_t::mapped_type& headerValue); void setRequestHeaders(const headers_t& headers); - const QUrlQuery& query() const; + QUrlQuery query() const; void setRequestQuery(const QUrlQuery& query); const RequestData& requestData() const; void setRequestData(RequestData&& data); -- 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/events/encryptionevent.cpp | 8 ++++++++ lib/events/encryptionevent.h | 4 +--- lib/room.cpp | 9 +++++++++ lib/room.h | 6 ++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 490a5e8a..aa05a96e 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -39,6 +39,14 @@ EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) {} +EncryptionEventContent::EncryptionEventContent(EncryptionType et) + : encryption(et) +{ + if(encryption != Undefined) { + algorithm = encryptionStrings[encryption]; + } +} + void EncryptionEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 65ee4187..14439fcc 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -12,9 +12,7 @@ class EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; - explicit EncryptionEventContent(EncryptionType et = Undefined) - : encryption(et) - {} + explicit EncryptionEventContent(EncryptionType et = Undefined); explicit EncryptionEventContent(const QJsonObject& json); EncryptionType encryption; 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); +} diff --git a/lib/room.h b/lib/room.h index 4e55dbaf..c18d0b36 100644 --- a/lib/room.h +++ b/lib/room.h @@ -604,6 +604,12 @@ public Q_SLOTS: void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); + /** + * Activates encryption for this room. + * Warning: Cannot be undone + */ + void activateEncryption(); + Q_SIGNALS: /// Initial set of state events has been loaded /** -- cgit v1.2.3 From 693b5da2920f173a9e3f723b845d35a7b4aa9823 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 04:35:43 +0200 Subject: Add a download test to quotest --- quotest/CMakeLists.txt | 3 ++- quotest/quotest.cpp | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index bf9af796..59334e30 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -4,8 +4,9 @@ set(quotest_SRCS quotest.cpp) +find_package(${Qt} COMPONENTS Concurrent) add_executable(quotest ${quotest_SRCS}) -target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${PROJECT_NAME}) +target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME}) option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index ec7d4dcb..3f886676 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -5,6 +5,7 @@ #include "room.h" #include "user.h" #include "uriresolver.h" +#include "networkaccessmanager.h" #include "csapi/joining.h" #include "csapi/leaving.h" @@ -20,6 +21,8 @@ #include #include #include +#include +#include #include #include @@ -435,6 +438,27 @@ TEST_IMPL(sendFile) return false; } +bool testDownload(const QUrl& url) +{ + // Move out actual test from the multithreaded code + // to help debugging + auto results = + QtConcurrent::blockingMapped(QVector { 1, 2, 3 }, [url](int) { + QEventLoop el; + auto reply = + NetworkAccessManager::instance()->get(QNetworkRequest(url)); + QObject::connect( + reply, &QNetworkReply::finished, &el, [&el] { el.exit(); }, + Qt::QueuedConnection); + el.exec(); + return reply->error(); + }); + return std::all_of(results.cbegin(), results.cend(), + [](QNetworkReply::NetworkError ne) { + return ne == QNetworkReply::NoError; + }); +} + bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, const QString& txnId, const QString& fileName) @@ -465,14 +489,15 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return visit( *evt, [&](const RoomMessageEvent& e) { - // TODO: actually try to download it to check, e.g., #366 - // (and #368 would help to test against bad file names). + // TODO: check #366 once #368 is implemented FINISH_TEST( !e.id().isEmpty() - && pendingEvents[size_t(pendingIdx)]->transactionId() - == txnId - && e.hasFileContent() - && e.content()->fileInfo()->originalName == fileName); + && pendingEvents[size_t(pendingIdx)]->transactionId() + == txnId + && e.hasFileContent() + && e.content()->fileInfo()->originalName == fileName + && testDownload(targetRoom->connection()->makeMediaUrl( + e.content()->fileInfo()->url))); }, [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); }); -- cgit v1.2.3 From 2c61b463fa0608626a58aed79ebecb3bbd8c41d3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 04:36:13 +0200 Subject: NAM::createRequest(): more logging --- lib/networkaccessmanager.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index d35b2ec8..57618329 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -93,6 +93,7 @@ QNetworkReply* NetworkAccessManager::createRequest( // doesn't provide multithreading guarantees static thread_local QSettings s; if (!s.value("Network/allow_direct_media_requests").toBool()) { + qCWarning(NETWORK) << "No connection specified"; return new MxcReply(); } // TODO: Make the best effort with a direct unauthenticated request @@ -100,14 +101,14 @@ QNetworkReply* NetworkAccessManager::createRequest( } else { auto* const connection = AccountRegistry::instance().get(accountId); if (!connection) { - qCWarning(NETWORK) << "Connection not found"; + qCWarning(NETWORK) << "Connection" << accountId << "not found"; return new MxcReply(); } const auto roomId = query.queryItemValue(QStringLiteral("room_id")); if (!roomId.isEmpty()) { auto room = connection->room(roomId); if (!room) { - qCWarning(NETWORK) << "Room not found"; + qCWarning(NETWORK) << "Room" << roomId << "not found"; return new MxcReply(); } return new MxcReply( -- cgit v1.2.3 From 77b69eb370e1cdbc33e44121f4f8483e19cad7d8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 04:37:08 +0200 Subject: MxcReply: make sure to create a Private object --- lib/mxcreply.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index daa4af9a..0b6643fc 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -10,13 +10,16 @@ using namespace Quotient; class MxcReply::Private { public: - QNetworkReply *m_reply = nullptr; + explicit Private(QNetworkReply* r = nullptr) + : m_reply(r) + {} + QNetworkReply* m_reply; }; MxcReply::MxcReply(QNetworkReply* reply) + : d(std::make_unique(reply)) { reply->setParent(this); - d->m_reply = reply; connect(d->m_reply, &QNetworkReply::finished, this, [this]() { setError(d->m_reply->error(), d->m_reply->errorString()); setOpenMode(ReadOnly); @@ -25,10 +28,9 @@ MxcReply::MxcReply(QNetworkReply* reply) } MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) - : d(std::make_unique()) + : d(std::make_unique(reply)) { reply->setParent(this); - d->m_reply = reply; connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() { setError(d->m_reply->error(), d->m_reply->errorString()); setOpenMode(ReadOnly); -- cgit v1.2.3 From 06a4fbb5c0ad0fadba1e5924f73d067850a78312 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 04:38:23 +0200 Subject: Connection: update AccountRegistry Clients don't need to do it themselves. --- lib/connection.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 51946b2f..4abf5097 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -13,6 +13,7 @@ #include "room.h" #include "settings.h" #include "user.h" +#include "accountregistry.h" // NB: since Qt 6, moc_connection.cpp needs Room and User fully defined #include "moc_connection.cpp" @@ -258,6 +259,7 @@ Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); + AccountRegistry::instance().drop(this); } void Connection::resolveServer(const QString& mxid) @@ -441,6 +443,7 @@ void Connection::Private::completeSetup(const QString& mxId) qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << data->userId() << "from device" << data->deviceId(); + AccountRegistry::instance().add(q); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 167509514587aa22d837b42a8d30d7c1128e0a45 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 04:52:41 +0200 Subject: Fix building with older Qt --- quotest/quotest.cpp | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 3f886676..31a0b6d6 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -438,21 +438,30 @@ TEST_IMPL(sendFile) return false; } +// Can be replaced with a lambda once QtConcurrent is able to resolve return +// types from lambda invocations (Qt 6 can, not sure about earlier) +struct DownloadRunner { + QUrl url; + + using result_type = QNetworkReply::NetworkError; + + QNetworkReply::NetworkError operator()(int) const { + QEventLoop el; + auto reply = NetworkAccessManager::instance()->get(QNetworkRequest(url)); + QObject::connect( + reply, &QNetworkReply::finished, &el, [&el] { el.exit(); }, + Qt::QueuedConnection); + el.exec(); + return reply->error(); + } +}; + bool testDownload(const QUrl& url) { // Move out actual test from the multithreaded code // to help debugging - auto results = - QtConcurrent::blockingMapped(QVector { 1, 2, 3 }, [url](int) { - QEventLoop el; - auto reply = - NetworkAccessManager::instance()->get(QNetworkRequest(url)); - QObject::connect( - reply, &QNetworkReply::finished, &el, [&el] { el.exit(); }, - Qt::QueuedConnection); - el.exec(); - return reply->error(); - }); + auto results = QtConcurrent::blockingMapped(QVector { 1, 2, 3 }, + DownloadRunner { url }); return std::all_of(results.cbegin(), results.cend(), [](QNetworkReply::NetworkError ne) { return ne == QNetworkReply::NoError; -- cgit v1.2.3 From bcaab611840a0a2ad284e6f1e7c2f0b4de10222d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 05:22:53 +0200 Subject: Fix a memory leak in DownloadRunner --- quotest/quotest.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 31a0b6d6..4142c718 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -445,11 +445,14 @@ struct DownloadRunner { using result_type = QNetworkReply::NetworkError; - QNetworkReply::NetworkError operator()(int) const { + QNetworkReply::NetworkError operator()(int) const + { QEventLoop el; - auto reply = NetworkAccessManager::instance()->get(QNetworkRequest(url)); + QScopedPointer reply { + NetworkAccessManager::instance()->get(QNetworkRequest(url)) + }; QObject::connect( - reply, &QNetworkReply::finished, &el, [&el] { el.exit(); }, + reply.data(), &QNetworkReply::finished, &el, [&el] { el.exit(); }, Qt::QueuedConnection); el.exec(); return reply->error(); -- cgit v1.2.3 From 59c9ca720093f2931c2eee1c0d5806d7e2e0c85f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 13 Sep 2021 16:05:46 +0200 Subject: Add room types to RoomCreateEvent --- lib/events/roomcreateevent.cpp | 21 +++++++++++++++++++++ lib/events/roomcreateevent.h | 2 ++ lib/quotient_common.h | 10 ++++++++++ 3 files changed, 33 insertions(+) diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index 6558bade..ff93041c 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -5,6 +5,22 @@ using namespace Quotient; +template <> +struct Quotient::JsonConverter { + static RoomType load(const QJsonValue& jv) + { + const auto& roomTypeString = jv.toString(); + for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end(); + ++it) + if (roomTypeString == *it) + return RoomType(it - RoomTypeStrings.begin()); + + if (!roomTypeString.isEmpty()) + qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString; + return RoomType::Undefined; + } +}; + bool RoomCreateEvent::isFederated() const { return fromJson(contentJson()["m.federate"_ls]); @@ -26,3 +42,8 @@ bool RoomCreateEvent::isUpgrade() const { return contentJson().contains("predecessor"_ls); } + +RoomType RoomCreateEvent::roomType() const +{ + return fromJson(contentJson()["type"_ls]); +} diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 05e623ed..b3ad287c 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -4,6 +4,7 @@ #pragma once #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { class RoomCreateEvent : public StateEventBase { @@ -24,6 +25,7 @@ public: QString version() const; Predecessor predecessor() const; bool isUpgrade() const; + RoomType roomType() const; }; REGISTER_EVENT_TYPE(RoomCreateEvent) } // namespace Quotient diff --git a/lib/quotient_common.h b/lib/quotient_common.h index bb2e6a6b..4444a111 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -88,6 +88,16 @@ enum UriResolveResult : short { }; Q_ENUM_NS(UriResolveResult) +enum RoomType { + Space, + Undefined, +}; +Q_ENUM_NS(RoomType); + +constexpr inline auto RoomTypeStrings = make_array( + "m.space" +); + } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) -- cgit v1.2.3 From fe9425f313e7c172095ff9355743427337b7ea78 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 13 Sep 2021 19:14:19 +0200 Subject: Add the encryptedfile to the eventcontent --- lib/events/encryptedfile.h | 88 +++++++++++++++++++++++++++++++++++++++++ lib/events/eventcontent.cpp | 20 +++++++--- lib/events/eventcontent.h | 17 ++++++-- lib/events/roomavatarevent.h | 2 +- lib/events/roommessageevent.cpp | 8 ++-- 5 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 lib/events/encryptedfile.h diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h new file mode 100644 index 00000000..24ac9de1 --- /dev/null +++ b/lib/events/encryptedfile.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPl-2.1-or-later + +#pragma once + +#include "converters.h" + +namespace Quotient { +/** + * JSON Web Key object as specified in + * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes + * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. + */ +struct JWK +{ + Q_GADGET + Q_PROPERTY(QString kty MEMBER kty CONSTANT) + Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) + Q_PROPERTY(QString alg MEMBER alg CONSTANT) + Q_PROPERTY(QString k MEMBER k CONSTANT) + Q_PROPERTY(bool ext MEMBER ext CONSTANT) + +public: + QString kty; + QStringList keyOps; + QString alg; + QString k; + bool ext; +}; + +struct EncryptedFile +{ + Q_GADGET + Q_PROPERTY(QUrl url MEMBER url CONSTANT) + Q_PROPERTY(JWK key MEMBER key CONSTANT) + Q_PROPERTY(QString iv MEMBER iv CONSTANT) + Q_PROPERTY(QHash hashes MEMBER hashes CONSTANT) + Q_PROPERTY(QString v MEMBER v CONSTANT) + +public: + QUrl url; + JWK key; + QString iv; + QHash hashes; + QString v; +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const EncryptedFile& pod) + { + addParam<>(jo, QStringLiteral("url"), pod.url); + addParam<>(jo, QStringLiteral("key"), pod.key); + addParam<>(jo, QStringLiteral("iv"), pod.iv); + addParam<>(jo, QStringLiteral("hashes"), pod.hashes); + addParam<>(jo, QStringLiteral("v"), pod.v); + } + static void fillFrom(const QJsonObject& jo, EncryptedFile& pod) + { + fromJson(jo.value("url"_ls), pod.url); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("iv"_ls), pod.iv); + fromJson(jo.value("hashes"_ls), pod.hashes); + fromJson(jo.value("v"_ls), pod.v); + } +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const JWK& pod) + { + addParam<>(jo, QStringLiteral("kty"), pod.kty); + addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); + addParam<>(jo, QStringLiteral("alg"), pod.alg); + addParam<>(jo, QStringLiteral("k"), pod.k); + addParam<>(jo, QStringLiteral("ext"), pod.ext); + } + static void fillFrom(const QJsonObject& jo, JWK& pod) + { + fromJson(jo.value("kty"_ls), pod.kty); + fromJson(jo.value("key_ops"_ls), pod.keyOps); + fromJson(jo.value("alg"_ls), pod.alg); + fromJson(jo.value("k"_ls), pod.k); + fromJson(jo.value("ext"_ls), pod.ext); + } +}; +} // namespace Quotient diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 1f28f195..22878d4c 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -30,11 +30,12 @@ FileInfo::FileInfo(const QFileInfo &fi) } FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, - QString originalFilename) + Omittable file, QString originalFilename) : mimeType(mimeType) , url(move(u)) , payloadSize(payloadSize) , originalName(move(originalFilename)) + , file(file) { if (!isValid()) qCWarning(MESSAGES) @@ -44,6 +45,7 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, } FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, + const Omittable &file, QString originalFilename) : originalInfoJson(infoJson) , mimeType( @@ -51,7 +53,11 @@ FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, , url(move(mxcUrl)) , payloadSize(fromJson(infoJson["size"_ls])) , originalName(move(originalFilename)) + , file(file) { + if(url.isEmpty() && file.has_value()) { + url = file->url; + } if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } @@ -76,14 +82,15 @@ ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) {} ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, - QSize imageSize, const QString& originalFilename) - : FileInfo(mxcUrl, fileSize, type, originalFilename) + QSize imageSize, const Omittable &file, const QString& originalFilename) + : FileInfo(mxcUrl, fileSize, type, file, originalFilename) , imageSize(imageSize) {} ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, + const Omittable &file, const QString& originalFilename) - : FileInfo(mxcUrl, infoJson, originalFilename) + : FileInfo(mxcUrl, infoJson, file, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} @@ -96,9 +103,10 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("h"), imageSize.height()); } -Thumbnail::Thumbnail(const QJsonObject& infoJson) +Thumbnail::Thumbnail(const QJsonObject& infoJson, const Omittable &file) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), - infoJson["thumbnail_info"_ls].toObject()) + infoJson["thumbnail_info"_ls].toObject(), + file) {} void Thumbnail::fillInfoJson(QJsonObject* infoJson) const diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 40ec3a49..f609a603 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -12,6 +12,8 @@ #include #include +#include "encryptedfile.h" + class QFileInfo; namespace Quotient { @@ -80,8 +82,10 @@ namespace EventContent { explicit FileInfo(const QFileInfo& fi); explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, const QMimeType& mimeType = {}, + Omittable file = none, QString originalFilename = {}); FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, + const Omittable &file, QString originalFilename = {}); bool isValid() const; @@ -103,6 +107,7 @@ namespace EventContent { QUrl url; qint64 payloadSize; QString originalName; + Omittable file = none; }; template @@ -122,8 +127,10 @@ namespace EventContent { explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, const QMimeType& type = {}, QSize imageSize = {}, + const Omittable &file = none, const QString& originalFilename = {}); ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, + const Omittable &encryptedFile, const QString& originalFilename = {}); void fillInfoJson(QJsonObject* infoJson) const; @@ -142,7 +149,7 @@ namespace EventContent { class Thumbnail : public ImageInfo { public: Thumbnail() = default; // Allow empty thumbnails - Thumbnail(const QJsonObject& infoJson); + Thumbnail(const QJsonObject& infoJson, const Omittable &file = none); Thumbnail(const ImageInfo& info) : ImageInfo(info) {} using ImageInfo::ImageInfo; @@ -182,7 +189,7 @@ namespace EventContent { explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - json["filename"].toString()) + fromJson>(json["file"]), json["filename"].toString()) { // A small hack to facilitate links creation in QML. originalJson.insert("mediaId", InfoT::mediaId()); @@ -196,7 +203,11 @@ namespace EventContent { void fillJson(QJsonObject* json) const override { Q_ASSERT(json); - json->insert("url", InfoT::url.toString()); + if (!InfoT::file.has_value()) { + json->insert("url", InfoT::url.toString()); + } else { + json->insert("file", Quotient::toJson(*InfoT::file)); + } if (!InfoT::originalName.isEmpty()) json->insert("filename", InfoT::originalName); json->insert("info", toInfoJson(*this)); diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 3fa11a0f..8618ba31 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -25,7 +25,7 @@ public: const QSize& imageSize = {}, const QString& originalFilename = {}) : RoomAvatarEvent(EventContent::ImageContent { - mxcUrl, fileSize, mimeType, imageSize, originalFilename }) + mxcUrl, fileSize, mimeType, imageSize, none, originalFilename }) {} QUrl url() const { return content().url; } diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 71f85363..9b46594e 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -145,21 +145,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, - QImageReader(filePath).size(), + QImageReader(filePath).size(), none, file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, - QMediaResource(localUrl).resolution(), + QMediaResource(localUrl).resolution(), none, file.fileName()); if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, file.size(), mimeType, + return new AudioContent(localUrl, file.size(), mimeType, none, file.fileName()); } - return new FileContent(localUrl, file.size(), mimeType, file.fileName()); + return new FileContent(localUrl, file.size(), mimeType, none, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, -- cgit v1.2.3 From 9f43d34c590a825504b72be7f6b238d0ff2c915a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Sep 2021 04:39:44 +0200 Subject: Use C++ instead of commenting --- quotest/quotest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 4142c718..3a77eb01 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -51,7 +51,7 @@ private: QByteArrayList running {}, succeeded {}, failed {}; }; -using TestToken = QByteArray; // return value of QMetaMethod::name +using TestToken = decltype(std::declval().name()); Q_DECLARE_METATYPE(TestToken) // For now, the token itself is the test name but that may change. -- cgit v1.2.3 From 92024f20fd8cfe4adb586b1d477969e45ba9ab4d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Sep 2021 08:41:06 +0200 Subject: SyncData: drop a shortcut that led to ignoring invites Fixes #510. --- lib/syncdata.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index d3c270b5..4edc9564 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -173,12 +173,7 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) auto rooms = json.value("rooms"_ls).toObject(); auto totalRooms = 0; auto totalEvents = 0; - // The first comparison shortcuts the loop when not all states are there - // in the response (anything except "join" is only occasional, and "join" - // intentionally comes first in the enum). - for (size_t i = 0; - static_cast(i) < rooms.size() && i < JoinStateStrings.size(); ++i) - { + for (size_t i = 0; i < JoinStateStrings.size(); ++i) { // This assumes that MemberState values go over powers of 2: 1,2,4,... const auto joinState = JoinState(1U << i); const auto rs = rooms.value(JoinStateStrings[i]).toObject(); -- cgit v1.2.3 From 4c3efb82b9f47ca92973089413da994619ddeb5c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 03:18:39 +0200 Subject: AccountRegistry: minor code cleanup --- lib/accountregistry.cpp | 5 ++--- lib/accountregistry.h | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 3a022f14..a292ed45 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -36,8 +36,7 @@ bool AccountRegistry::contains(Connection *c) const return m_accounts.contains(c); } -AccountRegistry::AccountRegistry() -{} +AccountRegistry::AccountRegistry() = default; QVariant AccountRegistry::data(const QModelIndex &index, int role) const { @@ -95,4 +94,4 @@ Connection* AccountRegistry::get(const QString& userId) } } return nullptr; -} \ No newline at end of file +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h index e87da3e8..5efda459 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -42,4 +42,4 @@ private: QVector m_accounts; }; -} \ No newline at end of file +} -- cgit v1.2.3 From bd71b075e699ab4dda92d72bac77cbfeb6217625 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 3 Oct 2021 19:43:03 +0200 Subject: prettyPrint(): tighten up Matrix identifier regex It was too permissive on characters before the identifier and also allowed the domain name to start on dash, which should not occur. Closes #512. --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 3de1d169..8067f561 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -44,7 +44,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText) // https://matrix.org/docs/spec/appendices.html#identifier-grammar static const QRegularExpression MxIdRegExp( QStringLiteral( - R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"), + R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"), RegExpOptions); Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid() && MxIdRegExp.isValid()); -- cgit v1.2.3 From 062d534a0024959117e310f8c2a964434acb9fa0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 09:53:33 +0200 Subject: Add tests for prettyPrint() --- quotest/quotest.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 3a77eb01..44d82adf 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -108,6 +108,7 @@ private slots: TEST_DECL(addAndRemoveTag) TEST_DECL(markDirectChat) TEST_DECL(visitResources) + TEST_DECL(prettyPrintTests) // Add more tests above here public: @@ -140,7 +141,7 @@ private: // connectUntil() to break the QMetaObject::Connection upon finishing the test // item. #define FINISH_TEST(Condition) \ - return (finishTest(thisTest, Condition, __FILE__, __LINE__), true) + return (finishTest(thisTest, (Condition), __FILE__, __LINE__), true) #define FAIL_TEST() FINISH_TEST(false) @@ -824,6 +825,52 @@ TEST_IMPL(visitResources) FINISH_TEST(true); } +bool checkPrettyPrint( + std::initializer_list> tests) +{ + bool result = true; + for (const auto& [test, etalon] : tests) { + const auto is = prettyPrint(test).toStdString(); + const auto shouldBe = std::string("") + + etalon + ""; + if (is == shouldBe) + continue; + clog << is << " != " << shouldBe << endl; + result = false; + } + return result; +} + +TEST_IMPL(prettyPrintTests) +{ + const bool prettyPrintTestResult = checkPrettyPrint( + { { "https://www.matrix.org", + R"(https://www.matrix.org)" }, +// { "www.matrix.org", // Doesn't work yet +// R"(www.matrix.org)" }, + { "smb://somewhere/file", "smb://somewhere/file" }, // Disallowed scheme + { "https:/something", "https:/something" }, // Malformed URL + { "https://matrix.to/#/!roomid:example.org", + R"(https://matrix.to/#/!roomid:example.org)" }, + { "https://matrix.to/#/@user_id:example.org", + R"(https://matrix.to/#/@user_id:example.org)" }, + { "https://matrix.to/#/#roomalias:example.org", + R"(https://matrix.to/#/#roomalias:example.org)" }, + { "https://matrix.to/#/##ircroomalias:example.org", + R"(https://matrix.to/#/##ircroomalias:example.org)" }, + { "me@example.org", + R"(me@example.org)" }, + { "mailto:me@example.org", + R"(mailto:me@example.org)" }, + { "!room_id:example.org", + R"(!room_id:example.org)" }, + { "@user_id:example.org", + R"(@user_id:example.org)" }, + { "#room_alias:example.org", + R"(#room_alias:example.org)" } }); + FINISH_TEST(prettyPrintTestResult); +} + void TestManager::conclude() { // Clean up the room (best effort) -- cgit v1.2.3 From c49de691291147233f24c9db17c0c1a3e2b73dde Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 09:53:53 +0200 Subject: Further tighten the linkifier in prettyPrint() --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 8067f561..993152dd 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -33,7 +33,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText) // comma or dot static const QRegularExpression FullUrlRegExp( QStringLiteral( - R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] -- cgit v1.2.3 From 73c1c8747b0c00524239724bb7cf00776448c5c7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 09:54:45 +0200 Subject: quotient_common.h: remove a stray semicolon --- lib/quotient_common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 4444a111..13bf7246 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -92,7 +92,7 @@ enum RoomType { Space, Undefined, }; -Q_ENUM_NS(RoomType); +Q_ENUM_NS(RoomType) constexpr inline auto RoomTypeStrings = make_array( "m.space" -- cgit v1.2.3 From c16813c5209f0421ec773a98cf935a2eb2ea3d7c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 11:05:10 +0200 Subject: Move away wrap_in_function to private interface This has always been merely a workaround to enable connectUntil/connectSingleShot and was never intended to be used elsewhere, let alone in clients. --- lib/qt_connection_util.h | 17 ++++++++++++++--- lib/util.h | 12 ------------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index c6fa037a..9370d2eb 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -73,6 +73,17 @@ namespace _impl { }), connType); } + + // TODO: get rid of it as soon as Apple Clang gets proper deduction guides + // for std::function<> + // ...or consider using QtPrivate magic used by QObject::connect() + // ...for inspiration, also check a possible std::not_fn implementation + // at https://en.cppreference.com/w/cpp/utility/functional/not_fn + template + inline auto wrap_in_function(FnT&& f) + { + return typename function_traits::function_type(std::forward(f)); + } } // namespace _impl /*! \brief Create a connection that self-disconnects when its "slot" returns true @@ -90,7 +101,7 @@ inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, const FunctorT& slot, Qt::ConnectionType connType = Qt::AutoConnection) { - return _impl::connectUntil(sender, signal, context, wrap_in_function(slot), + return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot), connType); } @@ -101,7 +112,7 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, Qt::ConnectionType connType = Qt::AutoConnection) { return _impl::connectSingleShot( - sender, signal, context, wrap_in_function(slot), connType); + sender, signal, context, _impl::wrap_in_function(slot), connType); } // Specialisation for usual Qt slots passed as pointers-to-members. @@ -114,7 +125,7 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, { // TODO: when switching to C++20, use std::bind_front() instead return _impl::connectSingleShot(sender, signal, receiver, - wrap_in_function( + _impl::wrap_in_function( [receiver, slot](const ArgTs&... args) { (receiver->*slot)(args...); }), diff --git a/lib/util.h b/lib/util.h index 5bfe6841..c6171b91 100644 --- a/lib/util.h +++ b/lib/util.h @@ -219,18 +219,6 @@ template using fn_arg_t = std::tuple_element_t::arg_types>; -// TODO: get rid of it as soon as Apple Clang gets proper deduction guides -// for std::function<> -// ...or consider using QtPrivate magic used by QObject::connect() -// since wrap_in_function() is actually made for qt_connection_util.h -// ...for inspiration, also check a possible std::not_fn implementation at -// https://en.cppreference.com/w/cpp/utility/functional/not_fn -template -inline auto wrap_in_function(FnT&& f) -{ - return typename function_traits::function_type(std::forward(f)); -} - inline constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); -- cgit v1.2.3 From 677c73263f434d1564695a8128d75fefd1a7b50b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 11:06:05 +0200 Subject: Drop old compatibility code libQuotient 0.7 really requires Qt 5.12, nothing earlier will work. --- lib/qt_connection_util.h | 5 ----- lib/util.cpp | 3 --- 2 files changed, 8 deletions(-) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 9370d2eb..ffefb2a2 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -19,12 +19,7 @@ namespace _impl { decorated_slot_tt decoratedSlot, Qt::ConnectionType connType) { - // See https://bugreports.qt.io/browse/QTBUG-60339 -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - auto pc = std::make_shared(); -#else auto pc = std::make_unique(); -#endif auto& c = *pc; // Resolve a reference before pc is moved to lambda // Perfect forwarding doesn't work through signal-slot connections - diff --git a/lib/util.cpp b/lib/util.cpp index 993152dd..2dfb09a6 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -14,9 +14,6 @@ static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption -#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) - | QRegularExpression::OptimizeOnFirstUsageOption // Default since 5.12 -#endif | QRegularExpression::UseUnicodePropertiesOption; // Converts all that looks like a URL into HTML links -- cgit v1.2.3 From df5606ebd360d753b6261133254408aadbbb7f7f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 11:07:47 +0200 Subject: Make connectSingleShot() a tiny wrapper on Qt 6 Qt 6 has Qt::SingleShotConnection; connectSingleShot remains just for the sake of compatibility across Qt 5 and Qt 6. If you target Qt 6 only, feel free to use the Qt facility directly. --- lib/qt_connection_util.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index ffefb2a2..46294499 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -106,6 +106,11 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, const FunctorT& slot, Qt::ConnectionType connType = Qt::AutoConnection) { +#if QT_VERSION_MAJOR >= 6 + return QObject::connect(sender, signal, context, slot, + Qt::ConnectionType(connType + | Qt::SingleShotConnection)); +#else return _impl::connectSingleShot( sender, signal, context, _impl::wrap_in_function(slot), connType); } @@ -125,6 +130,7 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, (receiver->*slot)(args...); }), connType); +#endif } /*! \brief A guard pointer that disconnects an interested object upon destruction -- cgit v1.2.3 From 7c11f7fddbcc98e4b3b92060c475799d7518624c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 18:26:47 +0200 Subject: gtad.yaml: make _rightQuote example less trivial --- gtad/gtad.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 58e1909c..ee8a43fe 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -140,7 +140,7 @@ mustache: # Syntax elements used by GTAD # _quote: '"' # Common quote for left and right # _leftQuote: '"' -# _rightQuote: '"' +# _rightQuote: '"_ls' _comment: '//' copyrightName: Kitsune Ral copyrightEmail: -- cgit v1.2.3 From 96f31d7d8ed1c9ab905c24ac039079aea622f4dc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 18:39:21 +0200 Subject: BaseJob: percent-encode variable path parts This is meant to spare clients from having to percent-encode room aliases, v3 event ids etc. that happen to hit the endpoint path. It is unfair to expect clients to do that since they are not supposed to care about the shape of CS API, which parameter should be encoded in which way. The trick (together with the slightly updated GTAD configuration) is to percent-encode parts that happen to be QStrings and not `const char[]`'s while passing all constant parts as plain C character literals. This also allows to make it more certain that the path is correctly encoded by passing and storing QByteArray's wherever the path is already encoded, and only use QStrings (next to const char arrays) before that. Since the change alters the API contract (even if that contract was crappy), some crude detection of percent-encoded stuff on input is inserted; if input is already percent-encoded, a warning is put to the logs, alerting developers about the change. --- gtad/gtad.yaml | 4 ++-- gtad/operation.cpp.mustache | 2 -- lib/jobs/basejob.cpp | 51 +++++++++++++++++++++++++++++++-------------- lib/jobs/basejob.h | 21 ++++++++++++++++--- lib/jobs/syncjob.cpp | 2 +- 5 files changed, 56 insertions(+), 24 deletions(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index ee8a43fe..943ac013 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -190,8 +190,8 @@ mustache: joinedParamDef: "{{>maybeCrefType}} {{paramName}}{{>cjoin}}" passPathAndMaybeQuery: >- - QStringLiteral("{{basePathWithoutHost}}") - {{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, + makePath("{{basePathWithoutHost}}"{{#pathParts}}, + {{_}}{{/pathParts}}){{#queryParams?}}, queryTo{{camelCaseOperationId}}( {{#queryParams}}{{paramName}}{{>cjoin}}{{/queryParams}}){{/queryParams?}} diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache index 7f692e4a..3d26ec73 100644 --- a/gtad/operation.cpp.mustache +++ b/gtad/operation.cpp.mustache @@ -4,8 +4,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later }}{{>preamble}} #include "{{filenameBase}}.h" -#include - using namespace Quotient; {{#operations}}{{#operation}} {{#queryParams?}} diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 85066024..73762e4f 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -71,7 +71,7 @@ public: // Using an idiom from clang-tidy: // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html - Private(HttpVerb v, QString endpoint, const QUrlQuery& q, + Private(HttpVerb v, QByteArray endpoint, const QUrlQuery& q, RequestData&& data, bool nt) : verb(v) , apiEndpoint(std::move(endpoint)) @@ -106,7 +106,7 @@ public: // Contents for the network request HttpVerb verb; - QString apiEndpoint; + QByteArray apiEndpoint; QHash requestHeaders; QUrlQuery requestQuery; RequestData requestData; @@ -166,14 +166,36 @@ public: } }; -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, +inline bool isHex(QChar c) +{ + return c.isDigit() || (c >= u'A' && c <= u'F') || (c >= u'a' && c <= u'f'); +} + +QByteArray BaseJob::encodeIfParam(const QString& paramPart) +{ + const auto percentIndex = paramPart.indexOf('%'); + if (percentIndex != -1 && paramPart.size() > percentIndex + 2 + && isHex(paramPart[percentIndex + 1]) + && isHex(paramPart[percentIndex + 2])) { + qCWarning(JOBS) + << "Developers, upfront percent-encoding of job parameters is " + "deprecated since libQuotient 0.7; the string involved is" + << paramPart; + return QUrl(paramPart, QUrl::TolerantMode).toEncoded(); + } + return QUrl::toPercentEncoding(paramPart); +} + +BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, QUrlQuery {}, RequestData {}, needsToken) + : BaseJob(verb, name, std::move(endpoint), QUrlQuery {}, RequestData {}, + needsToken) {} -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, +BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, const QUrlQuery& query, RequestData&& data, bool needsToken) - : d(new Private(verb, endpoint, query, std::move(data), needsToken)) + : d(new Private(verb, std::move(endpoint), query, std::move(data), + needsToken)) { setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); @@ -194,13 +216,6 @@ QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); } bool BaseJob::isBackground() const { return d->inBackground; } -//const QString& BaseJob::apiEndpoint() const { return d->apiUrl.path(); } - -//void BaseJob::setApiEndpoint(const QString& apiEndpoint) -//{ -// d->apiEndpoint = apiEndpoint; -//} - const BaseJob::headers_t& BaseJob::requestHeaders() const { return d->requestHeaders; @@ -259,13 +274,17 @@ const QNetworkReply* BaseJob::reply() const { return d->reply.data(); } QNetworkReply* BaseJob::reply() { return d->reply.data(); } -QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path, +QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QByteArray& encodedPath, const QUrlQuery& query) { // Make sure the added path is relative even if it's not (the official // API definitions have the leading slash though it's not really correct). - baseUrl = baseUrl.resolved( - QUrl(path.mid(path.startsWith('/')), QUrl::TolerantMode)); + const auto pathUrl = + QUrl::fromEncoded(encodedPath.mid(encodedPath.startsWith('/')), + QUrl::StrictMode); + Q_ASSERT_X(pathUrl.isValid(), __FUNCTION__, + qPrintable(pathUrl.errorString())); + baseUrl = baseUrl.resolved(pathUrl); baseUrl.setQuery(query); return baseUrl; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 663c121c..81455307 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -9,6 +9,7 @@ #include "../converters.h" #include +#include class QNetworkReply; class QSslError; @@ -23,6 +24,14 @@ class BaseJob : public QObject { Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) Q_PROPERTY(int statusCode READ error NOTIFY statusChanged) + + static QByteArray encodeIfParam(const QString& paramPart); + template + static inline auto encodeIfParam(const char (&constPart)[N]) + { + return constPart; + } + public: /*! The status code of a job * @@ -70,6 +79,12 @@ public: }; Q_ENUM(StatusCode) + template + static QByteArray makePath(StrTs&&... parts) + { + return (QByteArray() % ... % encodeIfParam(parts)); + } + using Data #ifndef Q_CC_MSVC Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead") @@ -124,9 +139,9 @@ public: }; public: - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, bool needsToken = true); - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, const QUrlQuery& query, RequestData&& data = {}, bool needsToken = true); @@ -352,7 +367,7 @@ protected: * The function ensures exactly one '/' between the path component of * \p baseUrl and \p path. The query component of \p baseUrl is ignored. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& path, + static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath, const QUrlQuery& query = {}); /*! Prepares the job for execution diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 59a34ef3..9b1b46f0 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -10,7 +10,7 @@ static size_t jobId = 0; SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, const QString& presence) : BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId), - QStringLiteral("_matrix/client/r0/sync")) + "_matrix/client/r0/sync") { setLoggingCategory(SYNCJOB); QUrlQuery query; -- cgit v1.2.3 From 1fd25fe944b67c55435ed4d4d8fd1cbb0989bb5f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 4 Oct 2021 18:40:48 +0200 Subject: Regenerate CS API files upon the previous commit --- lib/csapi/account-data.cpp | 28 ++++++++-------- lib/csapi/admin.cpp | 8 ++--- lib/csapi/administrative_contact.cpp | 27 +++++++-------- lib/csapi/appservice_room_directory.cpp | 6 ++-- lib/csapi/banning.cpp | 7 ++-- lib/csapi/capabilities.cpp | 9 ++--- lib/csapi/content-repo.cpp | 41 ++++++++++------------- lib/csapi/create_room.cpp | 4 +-- lib/csapi/cross_signing.cpp | 7 ++-- lib/csapi/device_management.cpp | 19 +++++------ lib/csapi/directory.cpp | 26 ++++++--------- lib/csapi/event_context.cpp | 11 +++--- lib/csapi/filter.cpp | 13 +++----- lib/csapi/inviting.cpp | 5 +-- lib/csapi/joining.cpp | 6 ++-- lib/csapi/keys.cpp | 14 ++++---- lib/csapi/kicking.cpp | 4 +-- lib/csapi/knocking.cpp | 4 +-- lib/csapi/leaving.cpp | 12 +++---- lib/csapi/list_joined_rooms.cpp | 9 ++--- lib/csapi/list_public_rooms.cpp | 20 +++++------ lib/csapi/login.cpp | 9 ++--- lib/csapi/logout.cpp | 14 +++----- lib/csapi/message_pagination.cpp | 7 ++-- lib/csapi/notifications.cpp | 8 ++--- lib/csapi/openid.cpp | 6 ++-- lib/csapi/peeking_events.cpp | 7 ++-- lib/csapi/presence.cpp | 12 +++---- lib/csapi/profile.cpp | 28 ++++++---------- lib/csapi/pusher.cpp | 9 ++--- lib/csapi/pushrules.cpp | 59 +++++++++++++++------------------ lib/csapi/read_markers.cpp | 5 +-- lib/csapi/receipts.cpp | 6 ++-- lib/csapi/redaction.cpp | 6 ++-- lib/csapi/registration.cpp | 28 +++++++--------- lib/csapi/report_content.cpp | 6 ++-- lib/csapi/room_send.cpp | 6 ++-- lib/csapi/room_state.cpp | 6 ++-- lib/csapi/room_upgrades.cpp | 5 +-- lib/csapi/rooms.cpp | 41 ++++++++++------------- lib/csapi/search.cpp | 4 +-- lib/csapi/sso_login_redirect.cpp | 15 ++++----- lib/csapi/tags.cpp | 24 ++++++-------- lib/csapi/third_party_lookup.cpp | 41 ++++++++++------------- lib/csapi/third_party_membership.cpp | 5 +-- lib/csapi/to_device.cpp | 6 ++-- lib/csapi/typing.cpp | 6 ++-- lib/csapi/users.cpp | 4 +-- lib/csapi/versions.cpp | 7 ++-- lib/csapi/voip.cpp | 9 ++--- lib/csapi/wellknown.cpp | 7 ++-- lib/csapi/whoami.cpp | 9 ++--- 52 files changed, 265 insertions(+), 420 deletions(-) diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 80deb8f1..09fc8d40 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -4,15 +4,13 @@ #include "account-data.h" -#include - using namespace Quotient; SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/account_data/" % type) + makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + type)) { setRequestData(RequestData(toJson(content))); } @@ -21,14 +19,14 @@ QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/user/" - % userId % "/account_data/" % type); + makePath("/_matrix/client/r0", "/user/", + userId, "/account_data/", type)); } GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/account_data/" % type) + makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + type)) {} SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, @@ -36,8 +34,8 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/account_data/" % type) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) { setRequestData(RequestData(toJson(content))); } @@ -48,15 +46,15 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/user/" % userId % "/rooms/" % roomId - % "/account_data/" % type); + makePath("/_matrix/client/r0", "/user/", + userId, "/rooms/", roomId, + "/account_data/", type)); } GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/account_data/" % type) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) {} diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 9619c441..81dd0624 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -4,18 +4,16 @@ #include "admin.h" -#include - using namespace Quotient; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/admin/whois/" % userId); + makePath("/_matrix/client/r0", + "/admin/whois/", userId)); } GetWhoIsJob::GetWhoIsJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), - QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId) + makePath("/_matrix/client/r0", "/admin/whois/", userId)) {} diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index 04360299..589c9fc1 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -4,25 +4,22 @@ #include "administrative_contact.h" -#include - using namespace Quotient; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid")); } GetAccount3PIDsJob::GetAccount3PIDsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid") + makePath("/_matrix/client/r0", "/account/3pid")) {} Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid") + makePath("/_matrix/client/r0", "/account/3pid")) { QJsonObject _data; addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); @@ -32,7 +29,7 @@ Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/add") + makePath("/_matrix/client/r0", "/account/3pid/add")) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); @@ -44,7 +41,7 @@ Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer, const QString& idAccessToken, const QString& sid) : BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/bind") + makePath("/_matrix/client/r0", "/account/3pid/bind")) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); @@ -58,7 +55,7 @@ Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/delete") + makePath("/_matrix/client/r0", "/account/3pid/delete")) { QJsonObject _data; addParam(_data, QStringLiteral("id_server"), idServer); @@ -72,7 +69,7 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/unbind") + makePath("/_matrix/client/r0", "/account/3pid/unbind")) { QJsonObject _data; addParam(_data, QStringLiteral("id_server"), idServer); @@ -85,8 +82,8 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid/email/requestToken", + makePath("/_matrix/client/r0", + "/account/3pid/email/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -95,8 +92,8 @@ RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid/msisdn/requestToken", + makePath("/_matrix/client/r0", + "/account/3pid/msisdn/requestToken"), false) { setRequestData(RequestData(toJson(body))); diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index 4d87e4af..40d784c6 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -4,16 +4,14 @@ #include "appservice_room_directory.h" -#include - using namespace Quotient; UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob( const QString& networkId, const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"), - QStringLiteral("/_matrix/client/r0") - % "/directory/list/appservice/" % networkId % "/" % roomId) + makePath("/_matrix/client/r0", "/directory/list/appservice/", + networkId, "/", roomId)) { QJsonObject _data; addParam<>(_data, QStringLiteral("visibility"), visibility); diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 8e0add1a..472128bb 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -4,14 +4,12 @@ #include "banning.h" -#include - using namespace Quotient; BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/ban") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/ban")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); @@ -22,8 +20,7 @@ BanJob::BanJob(const QString& roomId, const QString& userId, UnbanJob::UnbanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/unban") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp index 33a53cad..bc21e462 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -4,20 +4,17 @@ #include "capabilities.h" -#include - using namespace Quotient; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/capabilities"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities")); } GetCapabilitiesJob::GetCapabilitiesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), - QStringLiteral("/_matrix/client/r0") % "/capabilities") + makePath("/_matrix/client/r0", "/capabilities")) { addExpectedKey("capabilities"); } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 2d82437b..6d1e38b6 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -4,8 +4,6 @@ #include "content-repo.h" -#include - using namespace Quotient; auto queryToUploadContent(const QString& filename) @@ -18,7 +16,7 @@ auto queryToUploadContent(const QString& filename) UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) : BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"), - QStringLiteral("/_matrix/media/r0") % "/upload", + makePath("/_matrix/media/r0", "/upload"), queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); @@ -37,17 +35,16 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/download/" % serverName % "/" - % mediaId, + makePath("/_matrix/media/r0", "/download/", + serverName, "/", mediaId), queryToGetContent(allowRemote)); } GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"), - QStringLiteral("/_matrix/media/r0") % "/download/" % serverName - % "/" % mediaId, + makePath("/_matrix/media/r0", "/download/", serverName, "/", + mediaId), queryToGetContent(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); @@ -67,9 +64,9 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/download/" % serverName % "/" - % mediaId % "/" % fileName, + makePath("/_matrix/media/r0", "/download/", + serverName, "/", mediaId, "/", + fileName), queryToGetContentOverrideName(allowRemote)); } @@ -78,8 +75,8 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& fileName, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"), - QStringLiteral("/_matrix/media/r0") % "/download/" % serverName - % "/" % mediaId % "/" % fileName, + makePath("/_matrix/media/r0", "/download/", serverName, "/", + mediaId, "/", fileName), queryToGetContentOverrideName(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); @@ -104,8 +101,7 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/" - % mediaId, + makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", mediaId), queryToGetContentThumbnail(width, height, method, allowRemote)); } @@ -114,8 +110,8 @@ GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, int height, const QString& method, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"), - QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName - % "/" % mediaId, + makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", + mediaId), queryToGetContentThumbnail(width, height, method, allowRemote), {}, false) { @@ -134,25 +130,24 @@ QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/preview_url", + makePath("/_matrix/media/r0", + "/preview_url"), queryToGetUrlPreview(url, ts)); } GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable ts) : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), - QStringLiteral("/_matrix/media/r0") % "/preview_url", + makePath("/_matrix/media/r0", "/preview_url"), queryToGetUrlPreview(url, ts)) {} QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/config"); + makePath("/_matrix/media/r0", "/config")); } GetConfigJob::GetConfigJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), - QStringLiteral("/_matrix/media/r0") % "/config") + makePath("/_matrix/media/r0", "/config")) {} diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index a94f9951..9aaef87f 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -4,8 +4,6 @@ #include "create_room.h" -#include - using namespace Quotient; CreateRoomJob::CreateRoomJob(const QString& visibility, @@ -18,7 +16,7 @@ CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& preset, Omittable isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/createRoom") + makePath("/_matrix/client/r0", "/createRoom")) { QJsonObject _data; addParam(_data, QStringLiteral("visibility"), visibility); diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp index ed2b15c0..1fa0e949 100644 --- a/lib/csapi/cross_signing.cpp +++ b/lib/csapi/cross_signing.cpp @@ -4,8 +4,6 @@ #include "cross_signing.h" -#include - using namespace Quotient; UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( @@ -13,8 +11,7 @@ UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( const Omittable& selfSigningKey, const Omittable& userSigningKey) : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), - QStringLiteral("/_matrix/client/r0") - % "/keys/device_signing/upload") + makePath("/_matrix/client/r0", "/keys/device_signing/upload")) { QJsonObject _data; addParam(_data, QStringLiteral("master_key"), masterKey); @@ -28,7 +25,7 @@ UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( const QHash>& signatures) : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/signatures/upload") + makePath("/_matrix/client/r0", "/keys/signatures/upload")) { setRequestData(RequestData(toJson(signatures))); } diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index eac9a545..da6dbc76 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -4,38 +4,35 @@ #include "device_management.h" -#include - using namespace Quotient; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/devices"); + makePath("/_matrix/client/r0", "/devices")); } GetDevicesJob::GetDevicesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), - QStringLiteral("/_matrix/client/r0") % "/devices") + makePath("/_matrix/client/r0", "/devices")) {} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/devices/" % deviceId); + makePath("/_matrix/client/r0", "/devices/", + deviceId)); } GetDeviceJob::GetDeviceJob(const QString& deviceId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/r0", "/devices/", deviceId)) {} UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/r0", "/devices/", deviceId)) { QJsonObject _data; addParam(_data, QStringLiteral("display_name"), displayName); @@ -45,7 +42,7 @@ UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable& auth) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/r0", "/devices/", deviceId)) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); @@ -55,7 +52,7 @@ DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), - QStringLiteral("/_matrix/client/r0") % "/delete_devices") + makePath("/_matrix/client/r0", "/delete_devices")) { QJsonObject _data; addParam<>(_data, QStringLiteral("devices"), devices); diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 25ea82e2..b351b4ef 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -4,14 +4,11 @@ #include "directory.h" -#include - using namespace Quotient; SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias) + makePath("/_matrix/client/r0", "/directory/room/", roomAlias)) { QJsonObject _data; addParam<>(_data, QStringLiteral("room_id"), roomId); @@ -21,41 +18,38 @@ SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/room/" % roomAlias); + makePath("/_matrix/client/r0", + "/directory/room/", roomAlias)); } GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias, + makePath("/_matrix/client/r0", "/directory/room/", roomAlias), false) {} QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/room/" % roomAlias); + makePath("/_matrix/client/r0", + "/directory/room/", roomAlias)); } DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias) + makePath("/_matrix/client/r0", "/directory/room/", roomAlias)) {} QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/aliases"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/aliases")); } GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/aliases") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases")) { addExpectedKey("aliases"); } diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 3f4cd61e..877838e2 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -4,8 +4,6 @@ #include "event_context.h" -#include - using namespace Quotient; auto queryToGetEventContext(Omittable limit, const QString& filter) @@ -22,9 +20,8 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& filter) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/context/" - % eventId, + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/context/", eventId), queryToGetEventContext(limit, filter)); } @@ -33,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId, Omittable limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/context/" % eventId, + makePath("/_matrix/client/r0", "/rooms/", roomId, "/context/", + eventId), queryToGetEventContext(limit, filter)) {} diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 6b8863cc..38c68be7 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -4,14 +4,11 @@ #include "filter.h" -#include - using namespace Quotient; DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/filter") + makePath("/_matrix/client/r0", "/user/", userId, "/filter")) { setRequestData(RequestData(toJson(filter))); addExpectedKey("filter_id"); @@ -21,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/user/" - % userId % "/filter/" % filterId); + makePath("/_matrix/client/r0", "/user/", + userId, "/filter/", filterId)); } GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) : BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/filter/" % filterId) + makePath("/_matrix/client/r0", "/user/", userId, "/filter/", + filterId)) {} diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 1e2554f4..39d24611 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -4,15 +4,12 @@ #include "inviting.h" -#include - using namespace Quotient; InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/invite") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index f5266f0b..373c1c6a 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -4,15 +4,13 @@ #include "joining.h" -#include - using namespace Quotient; JoinRoomByIdJob::JoinRoomByIdJob( const QString& roomId, const Omittable& thirdPartySigned, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/join") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/join")) { QJsonObject _data; addParam(_data, QStringLiteral("third_party_signed"), @@ -34,7 +32,7 @@ JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const Omittable& thirdPartySigned, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/join/" % roomIdOrAlias, + makePath("/_matrix/client/r0", "/join/", roomIdOrAlias), queryToJoinRoom(serverName)) { QJsonObject _data; diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index ba5d8e12..d6bd2fab 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -4,14 +4,12 @@ #include "keys.h" -#include - using namespace Quotient; UploadKeysJob::UploadKeysJob(const Omittable& deviceKeys, const QHash& oneTimeKeys) : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/upload") + makePath("/_matrix/client/r0", "/keys/upload")) { QJsonObject _data; addParam(_data, QStringLiteral("device_keys"), deviceKeys); @@ -23,7 +21,7 @@ UploadKeysJob::UploadKeysJob(const Omittable& deviceKeys, QueryKeysJob::QueryKeysJob(const QHash& deviceKeys, Omittable timeout, const QString& token) : BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/query") + makePath("/_matrix/client/r0", "/keys/query")) { QJsonObject _data; addParam(_data, QStringLiteral("timeout"), timeout); @@ -36,7 +34,7 @@ ClaimKeysJob::ClaimKeysJob( const QHash>& oneTimeKeys, Omittable timeout) : BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/claim") + makePath("/_matrix/client/r0", "/keys/claim")) { QJsonObject _data; addParam(_data, QStringLiteral("timeout"), timeout); @@ -57,13 +55,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/keys/changes", + makePath("/_matrix/client/r0", + "/keys/changes"), queryToGetKeysChanges(from, to)); } GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) : BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/changes", + makePath("/_matrix/client/r0", "/keys/changes"), queryToGetKeysChanges(from, to)) {} diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 7de5ce01..433e592c 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -4,14 +4,12 @@ #include "kicking.h" -#include - using namespace Quotient; KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/kick") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/kick")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp index 788bb378..73e13e6e 100644 --- a/lib/csapi/knocking.cpp +++ b/lib/csapi/knocking.cpp @@ -4,8 +4,6 @@ #include "knocking.h" -#include - using namespace Quotient; auto queryToKnockRoom(const QStringList& serverName) @@ -18,7 +16,7 @@ auto queryToKnockRoom(const QStringList& serverName) KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/knock/" % roomIdOrAlias, + makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias), queryToKnockRoom(serverName)) { QJsonObject _data; diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index f4c5f120..0e5386be 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -4,14 +4,11 @@ #include "leaving.h" -#include - using namespace Quotient; LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/leave") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave")) { QJsonObject _data; addParam(_data, QStringLiteral("reason"), reason); @@ -21,12 +18,11 @@ LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/forget"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/forget")); } ForgetRoomJob::ForgetRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/forget") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget")) {} diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index 8d7e267f..22ba04da 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -4,20 +4,17 @@ #include "list_joined_rooms.h" -#include - using namespace Quotient; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/joined_rooms"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms")); } GetJoinedRoomsJob::GetJoinedRoomsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/joined_rooms") + makePath("/_matrix/client/r0", "/joined_rooms")) { addExpectedKey("joined_rooms"); } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index a4bcb934..25f8da5c 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -4,31 +4,27 @@ #include "list_public_rooms.h" -#include - using namespace Quotient; QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/list/room/" % roomId); + makePath("/_matrix/client/r0", + "/directory/list/room/", roomId)); } GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/list/room/" - % roomId, + makePath("/_matrix/client/r0", "/directory/list/room/", roomId), false) {} SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/list/room/" - % roomId) + makePath("/_matrix/client/r0", "/directory/list/room/", roomId)) { QJsonObject _data; addParam(_data, QStringLiteral("visibility"), visibility); @@ -50,15 +46,15 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable limit, const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/publicRooms", + makePath("/_matrix/client/r0", + "/publicRooms"), queryToGetPublicRooms(limit, since, server)); } GetPublicRoomsJob::GetPublicRoomsJob(Omittable limit, const QString& since, const QString& server) : BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/publicRooms", + makePath("/_matrix/client/r0", "/publicRooms"), queryToGetPublicRooms(limit, since, server), {}, false) { addExpectedKey("chunk"); @@ -78,7 +74,7 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/publicRooms", + makePath("/_matrix/client/r0", "/publicRooms"), queryToQueryPublicRooms(server)) { QJsonObject _data; diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index a5bac9ea..71fd93c5 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -4,20 +4,17 @@ #include "login.h" -#include - using namespace Quotient; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/login"); + makePath("/_matrix/client/r0", "/login")); } GetLoginFlowsJob::GetLoginFlowsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), - QStringLiteral("/_matrix/client/r0") % "/login", false) + makePath("/_matrix/client/r0", "/login"), false) {} LoginJob::LoginJob(const QString& type, @@ -26,7 +23,7 @@ LoginJob::LoginJob(const QString& type, const QString& deviceId, const QString& initialDeviceDisplayName) : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), - QStringLiteral("/_matrix/client/r0") % "/login", false) + makePath("/_matrix/client/r0", "/login"), false) { QJsonObject _data; addParam<>(_data, QStringLiteral("type"), type); diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp index 9583b8ec..e8083e31 100644 --- a/lib/csapi/logout.cpp +++ b/lib/csapi/logout.cpp @@ -4,30 +4,26 @@ #include "logout.h" -#include - using namespace Quotient; QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/logout"); + makePath("/_matrix/client/r0", "/logout")); } LogoutJob::LogoutJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), - QStringLiteral("/_matrix/client/r0") % "/logout") + makePath("/_matrix/client/r0", "/logout")) {} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/logout/all"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all")); } LogoutAllJob::LogoutAllJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), - QStringLiteral("/_matrix/client/r0") % "/logout/all") + makePath("/_matrix/client/r0", "/logout/all")) {} diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 441e4dea..1a93b75b 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -4,8 +4,6 @@ #include "message_pagination.h" -#include - using namespace Quotient; auto queryToGetRoomEvents(const QString& from, const QString& to, @@ -28,7 +26,7 @@ QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)); } @@ -36,7 +34,6 @@ GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/messages", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)) {} diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index a38e46f5..1e523c6f 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -4,8 +4,6 @@ #include "notifications.h" -#include - using namespace Quotient; auto queryToGetNotifications(const QString& from, Omittable limit, @@ -23,8 +21,8 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/notifications", + makePath("/_matrix/client/r0", + "/notifications"), queryToGetNotifications(from, limit, only)); } @@ -32,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable limit, const QString& only) : BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"), - QStringLiteral("/_matrix/client/r0") % "/notifications", + makePath("/_matrix/client/r0", "/notifications"), queryToGetNotifications(from, limit, only)) { addExpectedKey("notifications"); diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 0447db79..5c93a2d7 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -4,15 +4,13 @@ #include "openid.h" -#include - using namespace Quotient; RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/openid/request_token") + makePath("/_matrix/client/r0", "/user/", userId, + "/openid/request_token")) { setRequestData(RequestData(toJson(body))); } diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index ad2f9afe..eb5d22fa 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -4,8 +4,6 @@ #include "peeking_events.h" -#include - using namespace Quotient; auto queryToPeekEvents(const QString& from, Omittable timeout, @@ -22,14 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/events", + makePath("/_matrix/client/r0", "/events"), queryToPeekEvents(from, timeout, roomId)); } PeekEventsJob::PeekEventsJob(const QString& from, Omittable timeout, const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("PeekEventsJob"), - QStringLiteral("/_matrix/client/r0") % "/events", + makePath("/_matrix/client/r0", "/events"), queryToPeekEvents(from, timeout, roomId)) {} diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 58d0d157..4f77c466 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -4,15 +4,12 @@ #include "presence.h" -#include - using namespace Quotient; SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), - QStringLiteral("/_matrix/client/r0") % "/presence/" % userId - % "/status") + makePath("/_matrix/client/r0", "/presence/", userId, "/status")) { QJsonObject _data; addParam<>(_data, QStringLiteral("presence"), presence); @@ -23,14 +20,13 @@ SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/presence/" % userId % "/status"); + makePath("/_matrix/client/r0", "/presence/", + userId, "/status")); } GetPresenceJob::GetPresenceJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), - QStringLiteral("/_matrix/client/r0") % "/presence/" % userId - % "/status") + makePath("/_matrix/client/r0", "/presence/", userId, "/status")) { addExpectedKey("presence"); } diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 745fa488..64ac84ca 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -4,15 +4,12 @@ #include "profile.h" -#include - using namespace Quotient; SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) : BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/displayname") + makePath("/_matrix/client/r0", "/profile/", userId, "/displayname")) { QJsonObject _data; addParam<>(_data, QStringLiteral("displayname"), displayname); @@ -22,21 +19,19 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId, QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId % "/displayname"); + makePath("/_matrix/client/r0", "/profile/", + userId, "/displayname")); } GetDisplayNameJob::GetDisplayNameJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/displayname", + makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"), false) {} SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/avatar_url") + makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url")) { QJsonObject _data; addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); @@ -46,25 +41,24 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId % "/avatar_url"); + makePath("/_matrix/client/r0", "/profile/", + userId, "/avatar_url")); } GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/avatar_url", + makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"), false) {} QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId); + makePath("/_matrix/client/r0", "/profile/", + userId)); } GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false) + makePath("/_matrix/client/r0", "/profile/", userId), false) {} diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index 028022c5..ef4b3767 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -4,20 +4,17 @@ #include "pusher.h" -#include - using namespace Quotient; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushers"); + makePath("/_matrix/client/r0", "/pushers")); } GetPushersJob::GetPushersJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), - QStringLiteral("/_matrix/client/r0") % "/pushers") + makePath("/_matrix/client/r0", "/pushers")) {} PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, @@ -26,7 +23,7 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& lang, const PusherData& data, const QString& profileTag, Omittable append) : BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"), - QStringLiteral("/_matrix/client/r0") % "/pushers/set") + makePath("/_matrix/client/r0", "/pushers/set")) { QJsonObject _data; addParam<>(_data, QStringLiteral("pushkey"), pushkey); diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index ab7d0038..0d840788 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -4,20 +4,17 @@ #include "pushrules.h" -#include - using namespace Quotient; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules")); } GetPushRulesJob::GetPushRulesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules") + makePath("/_matrix/client/r0", "/pushrules")) { addExpectedKey("global"); } @@ -26,16 +23,15 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId)); } GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId) + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId)) {} QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, @@ -43,16 +39,15 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId)); } DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId) + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId)) {} auto queryToSetPushRule(const QString& before, const QString& after) @@ -70,8 +65,8 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QVector& conditions, const QString& pattern) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId, + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId), queryToSetPushRule(before, after)) { QJsonObject _data; @@ -86,17 +81,17 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId % "/enabled"); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId, + "/enabled")); } IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId % "/enabled") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { addExpectedKey("enabled"); } @@ -105,8 +100,8 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId % "/enabled") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { QJsonObject _data; addParam<>(_data, QStringLiteral("enabled"), enabled); @@ -118,17 +113,17 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId % "/actions"); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId, + "/actions")); } GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId % "/actions") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { addExpectedKey("actions"); } @@ -138,8 +133,8 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& ruleId, const QVector& actions) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId % "/actions") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { QJsonObject _data; addParam<>(_data, QStringLiteral("actions"), actions); diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index 39e4d148..f2edb71e 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -4,16 +4,13 @@ #include "read_markers.h" -#include - using namespace Quotient; SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/read_markers") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/read_markers")) { QJsonObject _data; addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 47b18174..401c3bfe 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -4,16 +4,14 @@ #include "receipts.h" -#include - using namespace Quotient; PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt) : BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/receipt/" % receiptType % "/" % eventId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/", + receiptType, "/", eventId)) { setRequestData(RequestData(toJson(receipt))); } diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 91497064..acf1b0e4 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -4,15 +4,13 @@ #include "redaction.h" -#include - using namespace Quotient; RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) : BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/redact/" % eventId % "/" % txnId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/", + eventId, "/", txnId)) { QJsonObject _data; addParam(_data, QStringLiteral("reason"), reason); diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index c3617bfc..153abcee 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -4,8 +4,6 @@ #include "registration.h" -#include - using namespace Quotient; auto queryToRegister(const QString& kind) @@ -22,7 +20,7 @@ RegisterJob::RegisterJob(const QString& kind, const QString& initialDeviceDisplayName, Omittable inhibitLogin) : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), - QStringLiteral("/_matrix/client/r0") % "/register", + makePath("/_matrix/client/r0", "/register"), queryToRegister(kind), {}, false) { QJsonObject _data; @@ -40,8 +38,7 @@ RegisterJob::RegisterJob(const QString& kind, RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/register/email/requestToken", + makePath("/_matrix/client/r0", "/register/email/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -50,8 +47,7 @@ RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/register/msisdn/requestToken", + makePath("/_matrix/client/r0", "/register/msisdn/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -61,7 +57,7 @@ ChangePasswordJob::ChangePasswordJob(const QString& newPassword, bool logoutDevices, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), - QStringLiteral("/_matrix/client/r0") % "/account/password") + makePath("/_matrix/client/r0", "/account/password")) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_password"), newPassword); @@ -74,8 +70,8 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/password/email/requestToken", + makePath("/_matrix/client/r0", + "/account/password/email/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -85,8 +81,8 @@ RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/password/msisdn/requestToken", + makePath("/_matrix/client/r0", + "/account/password/msisdn/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -95,7 +91,7 @@ RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( DeactivateAccountJob::DeactivateAccountJob( const Omittable& auth, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), - QStringLiteral("/_matrix/client/r0") % "/account/deactivate") + makePath("/_matrix/client/r0", "/account/deactivate")) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); @@ -115,13 +111,13 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/register/available", + makePath("/_matrix/client/r0", + "/register/available"), queryToCheckUsernameAvailability(username)); } CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), - QStringLiteral("/_matrix/client/r0") % "/register/available", + makePath("/_matrix/client/r0", "/register/available"), queryToCheckUsernameAvailability(username), {}, false) {} diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index ea906380..0a76d5b8 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -4,15 +4,13 @@ #include "report_content.h" -#include - using namespace Quotient; ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, Omittable score, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/report/" % eventId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/", + eventId)) { QJsonObject _data; addParam(_data, QStringLiteral("score"), score); diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 9fd8cb96..f80f9300 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -4,15 +4,13 @@ #include "room_send.h" -#include - using namespace Quotient; SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/send/" % eventType % "/" % txnId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/", + eventType, "/", txnId)) { setRequestData(RequestData(toJson(body))); addExpectedKey("event_id"); diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 37e897fa..f6d2e6ec 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -4,8 +4,6 @@ #include "room_state.h" -#include - using namespace Quotient; SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, @@ -13,8 +11,8 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& stateKey, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/state/" % eventType % "/" % stateKey) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) { setRequestData(RequestData(toJson(body))); addExpectedKey("event_id"); diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index e3791b08..d4129cfb 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -4,14 +4,11 @@ #include "room_upgrades.h" -#include - using namespace Quotient; UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/upgrade") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/upgrade")) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_version"), newVersion); diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 3dd87021..5310aa32 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -4,24 +4,21 @@ #include "rooms.h" -#include - using namespace Quotient; QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/event/" - % eventId); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/event/", eventId)); } GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) : BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/event/" % eventId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/", + eventId)) {} QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, @@ -29,30 +26,29 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/state/" - % eventType % "/" % stateKey); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/state/", eventType, "/", + stateKey)); } GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/state/" % eventType % "/" % stateKey) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) {} QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/state"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/state")); } GetRoomStateJob::GetRoomStateJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/state") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/state")) {} auto queryToGetMembersByRoom(const QString& at, const QString& membership, @@ -72,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/members", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)); } @@ -81,8 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/members", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)) {} @@ -90,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/joined_members"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/joined_members")); } GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/joined_members") + makePath("/_matrix/client/r0", "/rooms/", roomId, + "/joined_members")) {} diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 05ad871e..295dd1cc 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -4,8 +4,6 @@ #include "search.h" -#include - using namespace Quotient; auto queryToSearch(const QString& nextBatch) @@ -18,7 +16,7 @@ auto queryToSearch(const QString& nextBatch) SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), - QStringLiteral("/_matrix/client/r0") % "/search", + makePath("/_matrix/client/r0", "/search"), queryToSearch(nextBatch)) { QJsonObject _data; diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index 92601b4d..871d6ff6 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -4,8 +4,6 @@ #include "sso_login_redirect.h" -#include - using namespace Quotient; auto queryToRedirectToSSO(const QString& redirectUrl) @@ -18,14 +16,14 @@ auto queryToRedirectToSSO(const QString& redirectUrl) QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/login/sso/redirect", + makePath("/_matrix/client/r0", + "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl)); } RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"), - QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect", + makePath("/_matrix/client/r0", "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl), {}, false) {} @@ -40,15 +38,14 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/login/sso/redirect/" % idpId, + makePath("/_matrix/client/r0", + "/login/sso/redirect/", idpId), queryToRedirectToIdP(redirectUrl)); } RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), - QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect/" - % idpId, + makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId), queryToRedirectToIdP(redirectUrl), {}, false) {} diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index dc22dc18..f717de6e 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -4,30 +4,28 @@ #include "tags.h" -#include - using namespace Quotient; QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/user/" - % userId % "/rooms/" % roomId % "/tags"); + makePath("/_matrix/client/r0", "/user/", + userId, "/rooms/", roomId, "/tags")); } GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/tags") + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/tags")) {} SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable order, const QVariantHash& additionalProperties) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/tags/" % tag) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) { QJsonObject _data; fillJson(_data, additionalProperties); @@ -39,14 +37,14 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/user/" % userId % "/rooms/" % roomId - % "/tags/" % tag); + makePath("/_matrix/client/r0", "/user/", + userId, "/rooms/", roomId, "/tags/", + tag)); } DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/tags/" % tag) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) {} diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 93687a76..4c930668 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -4,34 +4,31 @@ #include "third_party_lookup.h" -#include - using namespace Quotient; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/protocols"); + makePath("/_matrix/client/r0", + "/thirdparty/protocols")); } GetProtocolsJob::GetProtocolsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols") + makePath("/_matrix/client/r0", "/thirdparty/protocols")) {} QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, const QString& protocol) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/protocol/" % protocol); + makePath("/_matrix/client/r0", + "/thirdparty/protocol/", protocol)); } GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/" - % protocol) + makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol)) {} auto queryToQueryLocationByProtocol(const QString& searchFields) @@ -46,16 +43,15 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/location/" % protocol, + makePath("/_matrix/client/r0", + "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)); } QueryLocationByProtocolJob::QueryLocationByProtocolJob( const QString& protocol, const QString& searchFields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/location/" - % protocol, + makePath("/_matrix/client/r0", "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)) {} @@ -71,16 +67,15 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/user/" % protocol, + makePath("/_matrix/client/r0", + "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)); } QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/user/" - % protocol, + makePath("/_matrix/client/r0", "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)) {} @@ -94,14 +89,14 @@ auto queryToQueryLocationByAlias(const QString& alias) QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/location", + makePath("/_matrix/client/r0", + "/thirdparty/location"), queryToQueryLocationByAlias(alias)); } QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/location", + makePath("/_matrix/client/r0", "/thirdparty/location"), queryToQueryLocationByAlias(alias)) {} @@ -115,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid) QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/user", + makePath("/_matrix/client/r0", + "/thirdparty/user"), queryToQueryUserByID(userid)); } QueryUserByIDJob::QueryUserByIDJob(const QString& userid) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/user", + makePath("/_matrix/client/r0", "/thirdparty/user"), queryToQueryUserByID(userid)) {} diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index fda772d2..59275e41 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -4,16 +4,13 @@ #include "third_party_membership.h" -#include - using namespace Quotient; InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& idAccessToken, const QString& medium, const QString& address) : BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/invite") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) { QJsonObject _data; addParam<>(_data, QStringLiteral("id_server"), idServer); diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 3775174d..628e8314 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -4,16 +4,14 @@ #include "to_device.h" -#include - using namespace Quotient; SendToDeviceJob::SendToDeviceJob( const QString& eventType, const QString& txnId, const QHash>& messages) : BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/sendToDevice/" - % eventType % "/" % txnId) + makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/", + txnId)) { QJsonObject _data; addParam<>(_data, QStringLiteral("messages"), messages); diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index 8e214053..c9673118 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -4,15 +4,13 @@ #include "typing.h" -#include - using namespace Quotient; SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable timeout) : BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/typing/" % userId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/", + userId)) { QJsonObject _data; addParam<>(_data, QStringLiteral("typing"), typing); diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index a0279d7e..48b727f0 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -4,14 +4,12 @@ #include "users.h" -#include - using namespace Quotient; SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable limit) : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), - QStringLiteral("/_matrix/client/r0") % "/user_directory/search") + makePath("/_matrix/client/r0", "/user_directory/search")) { QJsonObject _data; addParam<>(_data, QStringLiteral("search_term"), searchTerm); diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 9003e27f..a1efc33e 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -4,20 +4,17 @@ #include "versions.h" -#include - using namespace Quotient; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client") - % "/versions"); + makePath("/_matrix/client", "/versions")); } GetVersionsJob::GetVersionsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetVersionsJob"), - QStringLiteral("/_matrix/client") % "/versions", false) + makePath("/_matrix/client", "/versions"), false) { addExpectedKey("versions"); } diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 43170057..c748ad94 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -4,18 +4,15 @@ #include "voip.h" -#include - using namespace Quotient; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/voip/turnServer"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer")); } GetTurnServerJob::GetTurnServerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), - QStringLiteral("/_matrix/client/r0") % "/voip/turnServer") + makePath("/_matrix/client/r0", "/voip/turnServer")) {} diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index 1aa0a90b..0b441279 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -4,18 +4,15 @@ #include "wellknown.h" -#include - using namespace Quotient; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/.well-known") - % "/matrix/client"); + makePath("/.well-known", "/matrix/client")); } GetWellknownJob::GetWellknownJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetWellknownJob"), - QStringLiteral("/.well-known") % "/matrix/client", false) + makePath("/.well-known", "/matrix/client"), false) {} diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index 73f0298e..ed8a9817 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -4,20 +4,17 @@ #include "whoami.h" -#include - using namespace Quotient; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/account/whoami"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami")); } GetTokenOwnerJob::GetTokenOwnerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), - QStringLiteral("/_matrix/client/r0") % "/account/whoami") + makePath("/_matrix/client/r0", "/account/whoami")) { addExpectedKey("user_id"); } -- 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(-) 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 67da887e864d292608e7132388f518596374af34 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 5 Oct 2021 03:43:22 +0200 Subject: BaseJob::StatusCode: officially deprecate most *Error enumerators --- lib/connection.cpp | 8 ++++---- lib/jobs/basejob.cpp | 42 +++++++++++++++++++++--------------------- lib/jobs/basejob.h | 34 ++++++++++++++++------------------ 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4abf5097..093362ab 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -288,7 +288,7 @@ void Connection::resolveServer(const QString& mxid) if (d->resolverJob->error() == BaseJob::Abandoned) return; - if (d->resolverJob->error() != BaseJob::NotFoundError) { + if (d->resolverJob->error() != BaseJob::NotFound) { if (!d->resolverJob->status().good()) { qCWarning(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; @@ -401,7 +401,7 @@ void Connection::reloadCapabilities() " disabling version upgrade recommendations to reduce noise"; }); connect(d->capabilitiesJob, &BaseJob::failure, this, [this] { - if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) + if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest) qCDebug(MAIN) << "Server doesn't support /capabilities;" " version upgrade recommendations won't be issued"; }); @@ -1058,7 +1058,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) connect(leaveJob, &BaseJob::result, this, [this, leaveJob, forgetJob, room] { if (leaveJob->error() == BaseJob::Success - || leaveJob->error() == BaseJob::NotFoundError) { + || leaveJob->error() == BaseJob::NotFound) { run(forgetJob); // If the matching /sync response hasn't arrived yet, // mark the room for explicit deletion @@ -1077,7 +1077,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] { // Leave room in case of success, or room not known by server if (forgetJob->error() == BaseJob::Success - || forgetJob->error() == BaseJob::NotFoundError) + || forgetJob->error() == BaseJob::NotFound) d->removeRoom(id); // Delete the room from roomMap else qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 73762e4f..a7921c61 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -24,7 +24,7 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode) { // Based on https://en.wikipedia.org/wiki/List_of_HTTP_status_codes if (httpCode / 10 == 41) // 41x errors - return httpCode == 410 ? IncorrectRequestError : NotFoundError; + return httpCode == 410 ? IncorrectRequest : NotFound; switch (httpCode) { case 401: return Unauthorised; @@ -32,19 +32,19 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode) case 403: case 407: // clang-format on return ContentAccessError; case 404: - return NotFoundError; + return NotFound; // clang-format off case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on case 494: // Unofficial nginx "Request header too large" case 497: // Unofficial nginx "HTTP request sent to HTTPS port" - return IncorrectRequestError; + return IncorrectRequest; case 429: - return TooManyRequestsError; + return TooManyRequests; case 501: case 510: - return RequestNotImplementedError; + return RequestNotImplemented; case 511: - return NetworkAuthRequiredError; + return NetworkAuthRequired; default: return NetworkError; } @@ -365,7 +365,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground) qCCritical(d->logCat) << "Developers, ensure the Connection is valid before using it"; Q_ASSERT(false); - setStatus(IncorrectRequestError, tr("Invalid server connection")); + setStatus(IncorrectRequest, tr("Invalid server connection")); } // The status is no good, finalise QTimer::singleShot(0, this, &BaseJob::finishJob); @@ -528,7 +528,7 @@ BaseJob::Status BaseJob::prepareError() // of if's below will fall through to `return NoError` at the end const auto& errorJson = jsonData(); const auto errCode = errorJson.value("errcode"_ls).toString(); - if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { + if (error() == TooManyRequests || errCode == "M_LIMIT_EXCEEDED") { QString msg = tr("Too many requests"); int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1); if (retryAfterMs >= 0) @@ -538,16 +538,16 @@ BaseJob::Status BaseJob::prepareError() d->connection->limitRate(milliseconds(retryAfterMs)); - return { TooManyRequestsError, msg }; + return { TooManyRequests, msg }; } if (errCode == "M_CONSENT_NOT_GIVEN") { d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString()); - return { UserConsentRequiredError }; + return { UserConsentRequired }; } if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || errCode == "M_INCOMPATIBLE_ROOM_VERSION") - return { UnsupportedRoomVersionError, + return { UnsupportedRoomVersion, errorJson.contains("room_version"_ls) ? tr("Requested room version: %1") .arg(errorJson.value("room_version"_ls).toString()) @@ -729,27 +729,27 @@ QString BaseJob::statusCaption() const return tr("Request was abandoned"); case NetworkError: return tr("Network problems"); - case TimeoutError: + case Timeout: return tr("Request timed out"); case Unauthorised: return tr("Unauthorised request"); case ContentAccessError: return tr("Access error"); - case NotFoundError: + case NotFound: return tr("Not found"); - case IncorrectRequestError: + case IncorrectRequest: return tr("Invalid request"); - case IncorrectResponseError: + case IncorrectResponse: return tr("Response could not be parsed"); - case TooManyRequestsError: + case TooManyRequests: return tr("Too many requests"); - case RequestNotImplementedError: + case RequestNotImplemented: return tr("Function not implemented by the server"); - case NetworkAuthRequiredError: + case NetworkAuthRequired: return tr("Network authentication required"); - case UserConsentRequiredError: + case UserConsentRequired: return tr("User consent required"); - case UnsupportedRoomVersionError: + case UnsupportedRoomVersion: return tr("The server does not support the needed room version"); default: return tr("Request failed"); @@ -811,7 +811,7 @@ void BaseJob::abandon() void BaseJob::timeout() { - setStatus(TimeoutError, "The job has timed out"); + setStatus(Timeout, "The job has timed out"); finishJob(); } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 81455307..e0910a26 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -33,6 +33,10 @@ class BaseJob : public QObject { } public: +#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \ + Recommended, Recommended##Error Q_DECL_ENUMERATOR_DEPRECATED_X( \ + "Use " #Recommended) = Recommended + /*! The status code of a job * * Every job is created in Unprepared status; upon calling prepare() @@ -43,7 +47,7 @@ public: */ enum StatusCode { Success = 0, - NoError = Success, // To be compatible with Qt conventions + NoError = Success, Pending = 1, WarningLevel = 20, //< Warnings have codes starting from this UnexpectedResponseType = 21, @@ -52,26 +56,18 @@ public: Abandoned = 50, //< A tiny period between abandoning and object deletion ErrorLevel = 100, //< Errors have codes starting from this NetworkError = 101, - Timeout, - TimeoutError = Timeout, + WITH_DEPRECATED_ERROR_VERSION(Timeout), Unauthorised, ContentAccessError, - NotFoundError, - IncorrectRequest, - IncorrectRequestError = IncorrectRequest, - IncorrectResponse, - IncorrectResponseError = IncorrectResponse, - TooManyRequests, - TooManyRequestsError = TooManyRequests, + WITH_DEPRECATED_ERROR_VERSION(NotFound), + WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest), + WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse), + WITH_DEPRECATED_ERROR_VERSION(TooManyRequests), RateLimited = TooManyRequests, - RequestNotImplemented, - RequestNotImplementedError = RequestNotImplemented, - UnsupportedRoomVersion, - UnsupportedRoomVersionError = UnsupportedRoomVersion, - NetworkAuthRequired, - NetworkAuthRequiredError = NetworkAuthRequired, - UserConsentRequired, - UserConsentRequiredError = UserConsentRequired, + WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented), + WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion), + WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired), + WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired), CannotLeaveRoom, UserDeactivated, FileError, @@ -79,6 +75,8 @@ public: }; Q_ENUM(StatusCode) +#undef WITH_DEPRECATED_ERROR_VERSION + template static QByteArray makePath(StrTs&&... parts) { -- cgit v1.2.3 From 9cfd6ad6a4651280a12099d1a92432f07fc5aae0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 5 Oct 2021 03:44:18 +0200 Subject: BaseJob: refresh error handling - BaseJob::prepareError() slightly updated to get the current status instead of checking the returned value outside in gotReply() - BaseJob::gotReply() no more reports on 429 Too Many Requests twice (the first time with dubious "Too Many Requests: Unknown error") --- lib/jobs/basejob.cpp | 46 +++++++++++++++++++++++----------------------- lib/jobs/basejob.h | 6 ++++-- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a7921c61..971fea7b 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -415,42 +415,42 @@ BaseJob::Status BaseJob::Private::parseJson() void BaseJob::gotReply() { - setStatus(checkReply(reply())); - - if (status().good() - && d->expectedContentTypes == QByteArrayList { "application/json" }) { + // Defer actually updating the status until it's finalised + auto statusSoFar = checkReply(reply()); + if (statusSoFar.good() + && d->expectedContentTypes == QByteArrayList { "application/json" }) // + { d->rawResponse = reply()->readAll(); - setStatus(d->parseJson()); - if (status().good() && !expectedKeys().empty()) { + statusSoFar = d->parseJson(); + if (statusSoFar.good() && !expectedKeys().empty()) { const auto& responseObject = jsonData(); QByteArrayList missingKeys; for (const auto& k: expectedKeys()) if (!responseObject.contains(k)) missingKeys.push_back(k); if (!missingKeys.empty()) - setStatus(IncorrectResponse, tr("Required JSON keys missing: ") - + missingKeys.join()); + statusSoFar = { IncorrectResponse, + tr("Required JSON keys missing: ") + + missingKeys.join() }; } + setStatus(statusSoFar); if (!status().good()) // Bad JSON in a "good" reply: bail out return; - } // else { + } // If the endpoint expects anything else than just (API-related) JSON // reply()->readAll() is not performed and the whole reply processing // is left to derived job classes: they may read it piecemeal or customise // per content type in prepareResult(), or even have read it already // (see, e.g., DownloadFileJob). - // } - - if (status().good()) + if (statusSoFar.good()) { setStatus(prepareResult()); - else { - d->rawResponse = reply()->readAll(); - qCDebug(d->logCat).noquote() - << "Error body (truncated if long):" << rawDataSample(500); - // Parse the error payload and update the status if needed - if (const auto newStatus = prepareError(); !newStatus.good()) - setStatus(newStatus); + return; } + + d->rawResponse = reply()->readAll(); + qCDebug(d->logCat).noquote() + << "Error body (truncated if long):" << rawDataSample(500); + setStatus(prepareError(statusSoFar)); } bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) @@ -515,7 +515,7 @@ BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const BaseJob::Status BaseJob::prepareResult() { return Success; } -BaseJob::Status BaseJob::prepareError() +BaseJob::Status BaseJob::prepareError(Status currentStatus) { // Try to make sense of the error payload but be prepared for all kinds // of unexpected stuff (raw HTML, plain text, foreign JSON among those) @@ -525,7 +525,7 @@ BaseJob::Status BaseJob::prepareError() // By now, if d->parseJson() above succeeded then jsonData() will return // a valid JSON object - or an empty object otherwise (in which case most - // of if's below will fall through to `return NoError` at the end + // of if's below will fall through retaining the current status) const auto& errorJson = jsonData(); const auto errCode = errorJson.value("errcode"_ls).toString(); if (error() == TooManyRequests || errCode == "M_LIMIT_EXCEEDED") { @@ -560,9 +560,9 @@ BaseJob::Status BaseJob::prepareError() // Not localisable on the client side if (errorJson.contains("error"_ls)) // Keep the code, update the message - return { d->status.code, errorJson.value("error"_ls).toString() }; + return { currentStatus.code, errorJson.value("error"_ls).toString() }; - return NoError; // Retain the status if the error payload is not recognised + return currentStatus; // The error payload is not recognised } QJsonValue BaseJob::takeValueFromJson(const QString& key) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index e0910a26..119d7cce 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -399,10 +399,12 @@ protected: * was not good (usually because of an unsuccessful HTTP code). * The base implementation assumes Matrix JSON error object in the body; * overrides are strongly recommended to call it for all stock Matrix - * responses as early as possible but in addition can process custom errors, + * responses as early as possible and only then process custom errors, * with JSON or non-JSON payload. + * + * \return updated (if necessary) job status */ - virtual Status prepareError(); + virtual Status prepareError(Status currentStatus); /*! \brief Get direct access to the JSON response object in the job * -- cgit v1.2.3 From 107471447a62663eaf97b4b982d8c3f3e1b3364e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 5 Oct 2021 03:44:43 +0200 Subject: Connection: fix C++20 warnings --- lib/connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 093362ab..1fe0d2d0 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -341,7 +341,7 @@ void Connection::loginWithPassword(const QString& userId, const QString& initialDeviceName, const QString& deviceId) { - d->checkAndConnect(userId, [=] { + d->checkAndConnect(userId, [=,this] { d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId), password, /*token*/ "", deviceId, initialDeviceName); }, LoginFlows::Password); @@ -1716,7 +1716,7 @@ void Connection::getTurnServers() { auto job = callApi(); connect(job, &GetTurnServerJob::success, this, - [=] { emit turnServersChanged(job->data()); }); + [this,job] { emit turnServersChanged(job->data()); }); } const QString Connection::SupportedRoomVersion::StableTag = -- cgit v1.2.3 From 7b516cdf0b987e542b1e4cd4556ecb2bfbde3ff9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 5 Oct 2021 03:52:19 +0200 Subject: Quotest: return non-zero when things go really wrong ...such as stuck login or failure to join the room. Closes #496. --- quotest/quotest.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 44d82adf..d006c7fb 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -245,7 +245,7 @@ void TestManager::setupAndRun() clog << "Sync " << ++i << " complete" << endl; if (auto* r = testSuite->room()) { clog << "Test room timeline size = " << r->timelineSize(); - if (r->pendingEvents().empty()) + if (!r->pendingEvents().empty()) clog << ", pending size = " << r->pendingEvents().size(); clog << endl; } @@ -939,10 +939,22 @@ void TestManager::conclude() void TestManager::finalize() { + if (!c->isUsable() || !c->isLoggedIn()) { + clog << "No usable connection reached" << endl; + QCoreApplication::exit(-2); + return; // NB: QCoreApplication::exit() does return to the caller + } clog << "Logging out" << endl; c->logout(); - connect(c, &Connection::loggedOut, this, - [this] { QCoreApplication::exit(failed.size() + running.size()); }, + connect( + c, &Connection::loggedOut, this, + [this] { + QCoreApplication::exit(!testSuite ? -3 + : succeeded.empty() && failed.empty() + && running.empty() + ? -4 + : failed.size() + running.size()); + }, Qt::QueuedConnection); } -- 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(-) 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 762be1e73d6f5021e63fc9a4fea273951b3fb113 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 10 Oct 2021 22:26:08 +0200 Subject: ReadReceipt::operator==/!=: Add const where it's due --- lib/room.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/room.h b/lib/room.h index ca9f36cb..73f28a6e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -81,11 +81,14 @@ public: QString eventId; QDateTime timestamp; - bool operator==(const ReadReceipt& other) + bool operator==(const ReadReceipt& other) const { return eventId == other.eventId && timestamp == other.timestamp; } - bool operator!=(const ReadReceipt& other) { return !operator==(other); } + bool operator!=(const ReadReceipt& other) const + { + return !operator==(other); + } }; inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) { -- 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(-) 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 a1e885177bd9421a8ede4bec870a8fdc8b89a693 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 11 Oct 2021 16:02:21 +0200 Subject: formatJson(QDebug): Drop some very old cruft --- lib/logging.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/logging.h b/lib/logging.h index 7e0da975..5a3ef6ea 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -37,11 +37,7 @@ using QDebugManip = QDebug (*)(QDebug); */ inline QDebug formatJson(QDebug debug_object) { -#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) - return debug_object; -#else return debug_object.noquote(); -#endif } /** -- cgit v1.2.3 From fc0fdf2ed6006c11ffd47675fabb1232721c5e7d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 12 Oct 2021 09:26:09 +0200 Subject: RoomMemberEvent::is*(): fix comparison against Omittable Closes #514. --- lib/events/roommemberevent.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 8a6bddd8..469dbb32 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -103,16 +103,14 @@ bool RoomMemberEvent::isUnban() const bool RoomMemberEvent::isRename() const { - auto prevName = prevContent() && prevContent()->displayName - ? *prevContent()->displayName - : QString(); - return newDisplayName() != prevName; + return prevContent() && prevContent()->displayName + ? newDisplayName() != *prevContent()->displayName + : newDisplayName().has_value(); } bool RoomMemberEvent::isAvatarUpdate() const { - auto prevAvatarUrl = prevContent() && prevContent()->avatarUrl - ? *prevContent()->avatarUrl - : QUrl(); - return newAvatarUrl() != prevAvatarUrl; + return prevContent() && prevContent()->avatarUrl + ? newAvatarUrl() != *prevContent()->avatarUrl + : newAvatarUrl().has_value(); } -- cgit v1.2.3 From fd915b0865bde5741ceb1dd1e76a99d25b8c63fd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 12 Oct 2021 18:01:25 +0200 Subject: Omittable: add a deduction guide Just for completeness, not really needed anywhere yet. --- lib/util.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/util.h b/lib/util.h index c6171b91..9c146100 100644 --- a/lib/util.h +++ b/lib/util.h @@ -138,6 +138,8 @@ public: const value_type& operator*() const& { return base_type::operator*(); } value_type& operator*() && { return base_type::operator*(); } }; +template +Omittable(T&&) -> Omittable; namespace _impl { template -- cgit v1.2.3 From de164d60f2561fb0ad142b5d4fd31ff9817c2725 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 12 Oct 2021 19:17:42 +0200 Subject: Make sure to expose both the flags type and the underlying enum See also https://bugreports.qt.io/browse/QTBUG-82295. --- lib/quotient_common.h | 22 ++++++++++++++++++---- lib/room.h | 3 +-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 13bf7246..e1e14a14 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -7,6 +7,22 @@ #include +// See https://bugreports.qt.io/browse/QTBUG-82295 - despite the comment that +// Q_FLAG[_NS] "should" be applied to the enum only, Qt doesn't allow to wrap +// a flag type into a QVariant then. The macros below define Q_FLAG_NS and on +// top of that add a part of Q_ENUM() that enables the metatype data but goes +// under the moc radar to avoid double registration of the same data in the map +// defined in moc_*.cpp +#define QUO_DECLARE_FLAGS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_IMPL(Enum) \ + Q_FLAG(Flags) + +#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_NS_IMPL(Enum) \ + Q_FLAG_NS(Flags) + namespace Quotient { Q_NAMESPACE @@ -41,8 +57,7 @@ enum class Membership : unsigned int { Ban = 0x10, Undefined = Invalid }; -Q_DECLARE_FLAGS(MembershipMask, Membership) -Q_FLAG_NS(MembershipMask) +QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) constexpr inline auto MembershipStrings = make_array( // The order MUST be the same as the order in the original enum @@ -60,8 +75,7 @@ enum class JoinState : std::underlying_type_t { Invite = std::underlying_type_t(Membership::Invite), Knock = std::underlying_type_t(Membership::Knock), }; -Q_DECLARE_FLAGS(JoinStates, JoinState) -Q_FLAG_NS(JoinStates) +QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) constexpr inline auto JoinStateStrings = make_array( MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], diff --git a/lib/room.h b/lib/room.h index c4d94c02..b43217ae 100644 --- a/lib/room.h +++ b/lib/room.h @@ -148,8 +148,7 @@ public: OtherChange = 0x8000, AnyChange = 0xFFFF }; - Q_DECLARE_FLAGS(Changes, Change) - Q_FLAG(Changes) + QUO_DECLARE_FLAGS(Changes, Change) Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; -- cgit v1.2.3 From e633f9ed1558fe1e8aa026af2932a1d06b8beadb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 13 Oct 2021 10:00:15 +0200 Subject: CMakeLists: don't report that update-api is enabled when it's actually not add_feature_info() treats unset variable as "no change" rather than "false", which may lead to a confusing build configuration report when GTAD_PATH and/or MATRIX_DOC_PATH and/or CLANG_FORMAT have been there before but were removed since. --- CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bae833c3..30bab53a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,8 @@ set(FULL_CSAPI_DIR lib/${CSAPI_DIR}) set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) +set(API_GENERATION_ENABLED 0) +set(API_FORMATTING_ENABLED 0) if (GTAD_PATH AND MATRIX_DOC_PATH) get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" PROGRAM PROGRAM_ARGS GTAD_ARGS) if (EXISTS ${ABS_GTAD_PATH}) @@ -264,8 +266,10 @@ if (API_GENERATION_ENABLED) endif() add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" "build target update-api") -add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" - "formatting of generated API files with clang-format") +if (API_GENERATION_ENABLED) + add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" + "formatting of generated API files with clang-format") +endif() # Make no mistake: CMake cannot run gtad first and then populate the list of # resulting api_SRCS files. In other words, placing the below statement after -- cgit v1.2.3 From f620284bac23bb86c329863c53f3699845bf606f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 13 Oct 2021 10:16:39 +0200 Subject: CMakeLists: more robust GTAD_PATH detection After switching over to get_filename_component(PROGRAM) paths with ~ (home directory) were no more resolved. They are again. --- CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 30bab53a..34200548 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,7 +182,9 @@ set(ISAPI_DEF_DIR identity/definitions) set(API_GENERATION_ENABLED 0) set(API_FORMATTING_ENABLED 0) if (GTAD_PATH AND MATRIX_DOC_PATH) - get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" PROGRAM PROGRAM_ARGS GTAD_ARGS) + # REALPATH resolves ~ (home directory) while PROGRAM doesn't + get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) + get_filename_component(ABS_GTAD_PATH "${ABS_GTAD_PATH}" PROGRAM PROGRAM_ARGS GTAD_ARGS) if (EXISTS ${ABS_GTAD_PATH}) get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/data/api" REALPATH) if (NOT IS_DIRECTORY ${ABS_API_DEF_PATH}) @@ -195,7 +197,7 @@ if (GTAD_PATH AND MATRIX_DOC_PATH) message( WARNING "${MATRIX_DOC_PATH} doesn't seem to point to a valid matrix-doc repo; disabling API stubs generation") endif () else (EXISTS ${ABS_GTAD_PATH}) - message( WARNING "${GTAD_PATH} doesn't exist; disabling API stubs generation") + message( WARNING "${GTAD_PATH} is not executable; disabling API stubs generation") endif () endif () if (API_GENERATION_ENABLED) -- cgit v1.2.3 From b7de9c00b79ef0d187190aa5d126bff7503f471f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 13 Oct 2021 19:55:40 +0200 Subject: connection.h: more doc-comments --- lib/connection.h | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 1a6ca9b0..05a3bb7f 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -479,10 +479,23 @@ public: } public Q_SLOTS: - /** Set the homeserver base URL */ + /// \brief Set the homeserver base URL and retrieve its login flows + /// + /// \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged void setHomeserver(const QUrl& baseUrl); - /** Determine and set the homeserver from MXID */ + /// \brief Determine and set the homeserver from MXID + /// + /// This attempts to resolve the homeserver by requesting + /// .well-known/matrix/client record from the server taken from the MXID + /// serverpart. If there is no record found, the serverpart itself is + /// attempted as the homeserver base URL; if the record is there but + /// is malformed (e.g., the homeserver base URL cannot be found in it) + /// resolveError() is emitted and further processing stops. Otherwise, + /// setHomeserver is called, preparing the Connection object for the login + /// attempt. + /// \param mxid user Matrix ID, such as @someone:example.org + /// \sa setHomeserver, homeserverChanged, loginFlowsChanged, resolveError void resolveServer(const QString& mxid); /** \brief Log in using a username and password pair @@ -638,6 +651,11 @@ public Q_SLOTS: virtual LeaveRoomJob* leaveRoom(Room* room); Q_SIGNALS: + /// \brief Initial server resolution has failed + /// + /// This signal is emitted when resolveServer() did not manage to resolve + /// the homeserver using its .well-known/client record or otherwise. + /// \sa resolveServer void resolveError(QString error); void homeserverChanged(QUrl baseUrl); -- cgit v1.2.3 From 0aa70ed9b3cff3c9e0dbbea9beb419d840e42341 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 13 Oct 2021 19:57:01 +0200 Subject: Connection::resolveServer(): don't connect to loginFlowsJob Checking whether any login flows are available is a good enough measure of the homeserver actual workability. Closes #515. --- lib/connection.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 1fe0d2d0..2ad10694 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -316,10 +316,6 @@ void Connection::resolveServer(const QString& mxid) setHomeserver(maybeBaseUrl); } Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver() - connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { - qCWarning(MAIN) << "Homeserver base URL sanity check failed"; - emit resolveError(tr("The homeserver doesn't seem to be working")); - }); }); } @@ -478,10 +474,11 @@ void Connection::Private::checkAndConnect(const QString& userId, connectFn(); else emit q->loginError( + tr("Unsupported login flow"), tr("The homeserver at %1 does not support" " the login flow '%2'") - .arg(data->baseUrl().toDisplayString()), - flow->type); + .arg(data->baseUrl().toDisplayString(), + flow->type)); }); else connectSingleShot(q, &Connection::homeserverChanged, q, connectFn); -- cgit v1.2.3 From 9ee0efbf1d7a431c370b13e1d49c903165d474fe Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 17 Oct 2021 20:32:46 +0200 Subject: Room: qualify signal parameters --- lib/room.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/room.h b/lib/room.h index b43217ae..55dde2ee 100644 --- a/lib/room.h +++ b/lib/room.h @@ -623,11 +623,11 @@ Q_SIGNALS: */ void baseStateLoaded(); void eventsHistoryJobChanged(); - void aboutToAddHistoricalMessages(RoomEventsRange events); - void aboutToAddNewMessages(RoomEventsRange events); + void aboutToAddHistoricalMessages(Quotient::RoomEventsRange events); + void aboutToAddNewMessages(Quotient::RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); /// The event is about to be appended to the list of pending events - void pendingEventAboutToAdd(RoomEvent* event); + void pendingEventAboutToAdd(Quotient::RoomEvent* event); /// An event has been appended to the list of pending events void pendingEventAdded(); /// The remote echo has arrived with the sync and will be merged -- 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 ++++++++++ lib/room.h | 6 ++++++ 2 files changed, 16 insertions(+) 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 diff --git a/lib/room.h b/lib/room.h index 55dde2ee..452ea306 100644 --- a/lib/room.h +++ b/lib/room.h @@ -508,6 +508,12 @@ public: Q_INVOKABLE const Quotient::StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; + /// Get all state events in the room. + /*! This method returns all known state events that have occured in + * the room, as a mapping from the event type and state key to value. + */ + Q_INVOKABLE const QHash stateEvents() const; + /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of * its Matrix name. -- cgit v1.2.3 From 061de37889b0fa4bf8baae1f11693950297418c5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 8 Nov 2021 14:46:56 +0100 Subject: Q_DISABLE_MOVE/COPY_MOVE; QT_IGNORE_DEPRECATIONS DISABLE_MOVE is no more; instead, the library provides Q_DISABLE_MOVE (and also Q_DISABLE_COPY_MOVE while at it) for Qt pre-5.13 that don't have it yet. Same for QT_IGNORE_DEPRECATIONS - it only arrived in 5.15 but all the building pieces existed prior so libQuotient has it regardless of the Qt version used for building. --- lib/connection.cpp | 2 -- lib/events/event.h | 3 +-- lib/util.h | 28 ++++++++++++++++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2ad10694..75966731 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -77,8 +77,6 @@ public: explicit Private(std::unique_ptr&& connection) : data(move(connection)) {} - Q_DISABLE_COPY(Private) - DISABLE_MOVE(Private) Connection* q = nullptr; std::unique_ptr data; diff --git a/lib/events/event.h b/lib/events/event.h index f8f8311d..78853ced 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -76,8 +76,7 @@ public: private: EventTypeRegistry() = default; - Q_DISABLE_COPY(EventTypeRegistry) - DISABLE_MOVE(EventTypeRegistry) + Q_DISABLE_COPY_MOVE(EventTypeRegistry) static EventTypeRegistry& get() { diff --git a/lib/util.h b/lib/util.h index 9c146100..13efb94b 100644 --- a/lib/util.h +++ b/lib/util.h @@ -12,10 +12,30 @@ #include #include -// Along the lines of Q_DISABLE_COPY - the upstream version comes in Qt 5.13 -#define DISABLE_MOVE(_ClassName) \ - _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ - _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; +#ifndef Q_DISABLE_MOVE +// Q_DISABLE_MOVE was introduced in Q_VERSION_CHECK(5,13,0) +# define Q_DISABLE_MOVE(_ClassName) \ + _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ + _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; +#endif + +#ifndef Q_DISABLE_COPY_MOVE +#define Q_DISABLE_COPY_MOVE(Class) \ + Q_DISABLE_COPY(Class) \ + Q_DISABLE_MOVE(Class) +#endif + +#define DISABLE_MOVE(_ClassName) \ +static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all used versions of Qt"); + +#ifndef QT_IGNORE_DEPRECATIONS +// QT_IGNORE_DEPRECATIONS was introduced in Q_VERSION_CHECK(5,15,0) +# define QT_IGNORE_DEPRECATIONS(statement) \ + QT_WARNING_PUSH \ + QT_WARNING_DISABLE_DEPRECATED \ + statement \ + QT_WARNING_POP +#endif namespace Quotient { /// An equivalent of std::hash for QTypes to enable std::unordered_map -- 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(-) 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 +++++-- lib/room.h | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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; } diff --git a/lib/room.h b/lib/room.h index 55dde2ee..873c47d3 100644 --- a/lib/room.h +++ b/lib/room.h @@ -142,10 +142,13 @@ public: TagsChange = 0x40, MembersChange = 0x80, /* = 0x100, */ - AccountDataChange = 0x200, + AccountDataChange Q_DECL_ENUMERATOR_DEPRECATED_X( + "AccountDataChange will be merged into OtherChange in 0.8") = 0x200, SummaryChange = 0x400, - ReadMarkerChange = 0x800, + ReadMarkerChange Q_DECL_ENUMERATOR_DEPRECATED_X( + "ReadMarkerChange will be merged into OtherChange in 0.8") = 0x800, OtherChange = 0x8000, + OtherChanges = OtherChange, AnyChange = 0xFFFF }; QUO_DECLARE_FLAGS(Changes, Change) -- cgit v1.2.3 From 65bb0b5fcf029df7a9bfa0b7b7b7e3203fd7862f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 9 Nov 2021 13:59:47 +0100 Subject: DECL_DEPRECATED_ENUMERATOR A handy macro that introduces an enumerator with a respective Q_DECL_DEPRECATED_X recommending the substitution. --- lib/jobs/basejob.h | 6 +++--- lib/quotient_common.h | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 119d7cce..ddf243ed 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -7,6 +7,7 @@ #include "requestdata.h" #include "../logging.h" #include "../converters.h" +#include "../quotient_common.h" #include #include @@ -33,9 +34,8 @@ class BaseJob : public QObject { } public: -#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \ - Recommended, Recommended##Error Q_DECL_ENUMERATOR_DEPRECATED_X( \ - "Use " #Recommended) = Recommended +#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \ + Recommended, DECL_DEPRECATED_ENUMERATOR(Recommended##Error, Recommended) /*! The status code of a job * diff --git a/lib/quotient_common.h b/lib/quotient_common.h index e1e14a14..3d8ace67 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -23,6 +23,9 @@ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) +#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ + Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended + namespace Quotient { Q_NAMESPACE -- 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(-) 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 a4f34202b47f91f7fdfbe2006b2eae10b9b8eeac Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 11 Nov 2021 14:35:54 +0100 Subject: Fix a few quirks in converters.h --- lib/converters.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index cc6378e4..2df18b03 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -242,12 +242,11 @@ struct JsonObjectConverter> { for (const auto& e : s) json.insert(toJson(e), QJsonObject {}); } - static auto fillFrom(const QJsonObject& json, QSet& s) + static void fillFrom(const QJsonObject& json, QSet& s) { s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); - return s; } }; @@ -260,7 +259,7 @@ struct HashMapFromJson { } static void fillFrom(const QJsonObject& jo, HashMapT& h) { - h.reserve(jo.size()); + h.reserve(h.size() + jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson(it.value()); } -- 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(-) 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 05e71a72fdc6da3fb319edc67b723999285c9657 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 11 Nov 2021 14:35:54 +0100 Subject: Fix a few quirks in converters.h --- lib/converters.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index cc6378e4..2df18b03 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -242,12 +242,11 @@ struct JsonObjectConverter> { for (const auto& e : s) json.insert(toJson(e), QJsonObject {}); } - static auto fillFrom(const QJsonObject& json, QSet& s) + static void fillFrom(const QJsonObject& json, QSet& s) { s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); - return s; } }; @@ -260,7 +259,7 @@ struct HashMapFromJson { } static void fillFrom(const QJsonObject& jo, HashMapT& h) { - h.reserve(jo.size()); + h.reserve(h.size() + jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson(it.value()); } -- 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/converters.h | 7 +++++-- lib/events/receiptevent.cpp | 29 +++++++++++++++++++++++++---- lib/events/receiptevent.h | 5 +++-- lib/room.cpp | 27 +++++++-------------------- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index 2df18b03..d9a68bfb 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -134,7 +134,10 @@ struct JsonConverter : public TrivialJsonDumper { template <> struct JsonConverter { - static auto dump(const QDateTime& val) = delete; // not provided yet + static auto dump(const QDateTime& val) + { + return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); + } static auto load(const QJsonValue& jv) { return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); @@ -143,7 +146,7 @@ struct JsonConverter { template <> struct JsonConverter { - static auto dump(const QDate& val) = delete; // not provided yet + static auto dump(const QDate& val) { return toJson(QDateTime(val)); } static auto load(const QJsonValue& jv) { return fromJson(jv).date(); diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 4185d92d..72dbf2e3 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -25,6 +25,27 @@ Example of a Receipt Event: using namespace Quotient; +// The library loads the event-ids-to-receipts JSON map into a vector because +// map lookups are not used and vectors are massively faster. Same goes for +// de-/serialization of ReceiptsForEvent::receipts. +// (XXX: would this be generally preferred across CS API JSON maps?..) +QJsonObject toJson(const EventsWithReceipts& ewrs) +{ + QJsonObject json; + for (const auto& e : ewrs) { + QJsonObject receiptsJson; + for (const auto& r : e.receipts) + receiptsJson.insert(r.userId, + QJsonObject { { "ts"_ls, toJson(r.timestamp) } }); + json.insert(e.evtId, QJsonObject { { "m.read"_ls, receiptsJson } }); + } + return json; +} + +ReceiptEvent::ReceiptEvent(const EventsWithReceipts &ewrs) + : Event(typeId(), matrixTypeId(), toJson(ewrs)) +{} + EventsWithReceipts ReceiptEvent::eventsWithReceipts() const { EventsWithReceipts result; @@ -39,14 +60,14 @@ EventsWithReceipts ReceiptEvent::eventsWithReceipts() const } const auto reads = eventIt.value().toObject().value("m.read"_ls).toObject(); - QVector receipts; - receipts.reserve(reads.size()); + QVector usersAtEvent; + usersAtEvent.reserve(reads.size()); for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) { const auto user = userIt.value().toObject(); - receipts.push_back( + usersAtEvent.push_back( { userIt.key(), fromJson(user["ts"_ls]) }); } - result.push_back({ eventIt.key(), std::move(receipts) }); + result.push_back({ eventIt.key(), std::move(usersAtEvent) }); } return result; } diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index 4feec9ea..9683deef 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -9,19 +9,20 @@ #include namespace Quotient { -struct Receipt { +struct UserTimestamp { QString userId; QDateTime timestamp; }; struct ReceiptsForEvent { QString evtId; - QVector receipts; + QVector receipts; }; using EventsWithReceipts = QVector; class ReceiptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) + explicit ReceiptEvent(const EventsWithReceipts& ewrs); explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {} EventsWithReceipts eventsWithReceipts() const; 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 +-- lib/user.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) 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; } } diff --git a/lib/user.cpp b/lib/user.cpp index 88549e5d..7da71dba 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -66,7 +66,7 @@ User::~User() = default; void User::load() { auto* profileJob = - connection()->callApi(QUrl::toPercentEncoding(id())); + connection()->callApi(id()); connect(profileJob, &BaseJob::result, this, [this, profileJob] { d->defaultName = profileJob->displayname(); d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); -- 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(-) 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 +++++++++++++++++++++++++++++++++-------------------------- lib/room.h | 12 ++++- 2 files changed, 100 insertions(+), 75 deletions(-) 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()) diff --git a/lib/room.h b/lib/room.h index 260510e6..e4d33c4e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -442,10 +442,18 @@ public: //! This method is for cases when you need to show users who have read //! an event. Calling it on inexistent or empty event id will return //! an empty set. - //! \sa lastReadReceipt + //! \note The returned list may contain ids resolving to users that are + //! not loaded as room members yet (in particular, if members are not + //! yet lazy-loaded). For now this merely means that the user's + //! room-specific name and avatar will not be there; but generally + //! it's recommended to ensure that all room members are loaded + //! before operating on the result of this function. + //! \sa lastReadReceipt, allMembersLoaded + QSet userIdsAtEvent(const QString& eventId); + + [[deprecated("Use userIdsAtEvent instead")]] QSet usersAtEventId(const QString& eventId); - //! //! \brief Mark the event with uptoEventId as fully read //! //! Marks the event with the specified id as fully read locally and also -- cgit v1.2.3 From 7b633ba257fc8643ef8cc2ef724f3b6ac9e186ba Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 18 Oct 2021 05:42:12 +0200 Subject: Room: doc-comments cleanup [skip ci] --- lib/room.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/room.h b/lib/room.h index e4d33c4e..24025a88 100644 --- a/lib/room.h +++ b/lib/room.h @@ -381,8 +381,8 @@ public: void setLastDisplayedEvent(TimelineItem::index_t index); //! \brief Obtain a read receipt of any user + //! \deprecated Use lastReadReceipt or fullyReadMarker instead. //! - //! \deprecated Use lastReadReceipt or fullyReadMarker instead //! Historically, readMarker was returning a "converged" read marker //! representing both the read receipt and the fully read marker, as //! Quotient managed them together. Since 0.6.8, a single-argument call of @@ -397,16 +397,19 @@ public: rev_iter_t readMarker(const User* user) const; //! \brief Obtain the local user's fully-read marker //! \deprecated Use fullyReadMarker instead - //! See the documentation for the single-argument overload + //! + //! See the documentation for the single-argument overload. //! \sa fullyReadMarker [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker() const; //! \brief Get the event id for the local user's fully-read marker //! \deprecated Use lastFullyReadEventId instead + //! //! See the readMarker documentation [[deprecated("Use lastReadReceipt() to get m.read receipt or" - " fullyReadMarker() to get m.fully_read marker")]] // + " lastFullyReadEventId() to get an event id that" + " m.fully_read marker points to")]] // QString readMarkerEventId() const; //! \brief Get the latest read receipt from a user -- 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 ++++++++++ lib/room.h | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) 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 diff --git a/lib/room.h b/lib/room.h index 24025a88..d94de51c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -400,8 +400,7 @@ public: //! //! See the documentation for the single-argument overload. //! \sa fullyReadMarker - [[deprecated("Use lastReadReceipt() to get m.read receipt or" - " fullyReadMarker() to get m.fully_read marker")]] // + [[deprecated("Use localReadReceiptMarker() or fullyReadMarker()")]] // rev_iter_t readMarker() const; //! \brief Get the event id for the local user's fully-read marker //! \deprecated Use lastFullyReadEventId instead @@ -420,6 +419,19 @@ public: //! \sa usersAtEventId ReadReceipt lastReadReceipt(const QString& userId) const; + //! \brief Get the latest read receipt from the local user + //! + //! This is a shortcut for lastReadReceipt(localUserId). + //! \sa lastReadReceipt + ReadReceipt lastLocalReadReceipt() const; + + //! \brief Find the timeline item the local read receipt is at + //! + //! This is a shortcut for \code + //! room->findInTimeline(room->lastLocalReadReceipt().eventId); + //! \endcode + rev_iter_t localReadReceiptMarker() const; + //! \brief Get the latest event id marked as fully read //! //! This can be either the event id pointed to by the actual latest -- 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(-) 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 bcf7f7e6408872d8315e5c69829d7ce790e4820a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 17 Nov 2021 20:27:10 +0100 Subject: Fix QDateTime(QDate) deprecation warnings --- lib/converters.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/converters.h b/lib/converters.h index d9a68bfb..8ec0fa81 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -146,7 +146,15 @@ struct JsonConverter { template <> struct JsonConverter { - static auto dump(const QDate& val) { return toJson(QDateTime(val)); } + static auto dump(const QDate& val) { + return toJson( +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + QDateTime(val) +#else + val.startOfDay() +#endif + ); + } static auto load(const QJsonValue& jv) { return fromJson(jv).date(); -- 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 +++++++++++++++++++++++++++++------------------------------- lib/room.h | 39 ++++++++++++++++----------------- 2 files changed, 54 insertions(+), 55 deletions(-) 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; } diff --git a/lib/room.h b/lib/room.h index d94de51c..8a544e82 100644 --- a/lib/room.h +++ b/lib/room.h @@ -157,26 +157,27 @@ public: using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; - enum Change : uint { - NoChange = 0x0, - NameChange = 0x1, - AliasesChange = 0x2, - CanonicalAliasChange = AliasesChange, - TopicChange = 0x4, - UnreadNotifsChange = 0x8, - AvatarChange = 0x10, - JoinStateChange = 0x20, - TagsChange = 0x40, - MembersChange = 0x80, + enum class Change : uint { + None = 0x0, + Name = 0x1, + Aliases = 0x2, + CanonicalAlias = Aliases, + Topic = 0x4, + UnreadNotifs = 0x8, + Avatar = 0x10, + JoinState = 0x20, + Tags = 0x40, + Members = 0x80, /* = 0x100, */ - AccountDataChange Q_DECL_ENUMERATOR_DEPRECATED_X( - "AccountDataChange will be merged into OtherChange in 0.8") = 0x200, - SummaryChange = 0x400, - ReadMarkerChange Q_DECL_ENUMERATOR_DEPRECATED_X( - "ReadMarkerChange will be merged into OtherChange in 0.8") = 0x800, - OtherChange = 0x8000, - OtherChanges = OtherChange, - AnyChange = 0xFFFF + AccountData Q_DECL_ENUMERATOR_DEPRECATED_X( + "Change::AccountData will be merged into Change::Other in 0.8") = + 0x200, + Summary = 0x400, + ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X( + "Change::ReadMarker will be merged into Change::Other in 0.8") = + 0x800, + Other = 0x8000, + Any = 0xFFFF }; QUO_DECLARE_FLAGS(Changes, Change) -- 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`. --- CMakeLists.txt | 2 +- lib/connection.cpp | 4 ++-- lib/logging.h | 31 +++++++++++++++++++++---------- lib/quotient_common.cpp | 45 --------------------------------------------- lib/quotient_common.h | 6 ------ lib/room.cpp | 4 ++-- 6 files changed, 26 insertions(+), 66 deletions(-) delete mode 100644 lib/quotient_common.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 34200548..3814bc7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,7 +124,7 @@ endif () # Set up source files list(APPEND lib_SRCS - lib/quotient_common.cpp + lib/quotient_common.h lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp diff --git a/lib/connection.cpp b/lib/connection.cpp index 75966731..e65fdac4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -636,7 +636,7 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, } qWarning(MAIN) << "Room" << roomData.roomId << "has just been forgotten but /sync returned it in" - << roomData.joinState + << terse << roomData.joinState << "state - suspiciously fast turnaround"; } if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) { @@ -1341,7 +1341,7 @@ void Connection::Private::removeRoom(const QString& roomId) { for (auto f : { false, true }) if (auto r = roomMap.take({ roomId, f })) { - qCDebug(MAIN) << "Room" << r->objectName() << "in state" + qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse << r->joinState() << "will be deleted"; emit r->beforeDestruction(r); r->deleteLater(); diff --git a/lib/logging.h b/lib/logging.h index 5a3ef6ea..5bf050a9 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -40,17 +40,15 @@ inline QDebug formatJson(QDebug debug_object) return debug_object.noquote(); } -/** - * @brief A helper operator to facilitate usage of formatJson (and possibly - * other manipulators) - * - * @param debug_object to output the json to - * @param qdm a QDebug manipulator - * @return a copy of debug_object that has its mode altered by qdm - */ -inline QDebug operator<<(QDebug debug_object, QDebugManip qdm) +//! Suppress full qualification of enums/QFlags when logging +inline QDebug terse(QDebug dbg) { - return qdm(debug_object); + return +#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) + dbg.setVerbosity(0), dbg; +#else + dbg.verbosity(QDebug::MinimumVerbosity); +#endif } inline qint64 profilerMinNsecs() @@ -65,6 +63,19 @@ inline qint64 profilerMinNsecs() } } // namespace Quotient +/** + * @brief A helper operator to facilitate usage of formatJson (and possibly + * other manipulators) + * + * @param debug_object to output the json to + * @param qdm a QDebug manipulator + * @return a copy of debug_object that has its mode altered by qdm + */ +inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm) +{ + return qdm(debug_object); +} + inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) { auto val = et.nsecsElapsed() / 1000; diff --git a/lib/quotient_common.cpp b/lib/quotient_common.cpp deleted file mode 100644 index 5d7a3027..00000000 --- a/lib/quotient_common.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "quotient_common.h" - -#include - -using namespace Quotient; - -template -inline QDebug suppressScopeAndDump(QDebug dbg, Enum e) -{ - // Suppress "Quotient::" prefix - QDebugStateSaver _dss(dbg); - dbg.setVerbosity(0 /* QDebug::MinimumVerbosity since Qt 5.13 */); - return qt_QMetaEnum_debugOperator(dbg, std::underlying_type_t(e), - qt_getEnumMetaObject(e), - qt_getEnumName(e)); -} - -template -inline QDebug suppressScopeAndDump(QDebug dbg, const QFlags& f) -{ - // Suppress "Quotient::" prefix - QDebugStateSaver _dss(dbg); - dbg.setVerbosity(0 /* QDebug::MinimumVerbosity since Qt 5.13 */); - return qt_QMetaEnum_flagDebugOperator_helper(dbg, f); -} - -QDebug operator<<(QDebug dbg, Membership m) -{ - return suppressScopeAndDump(dbg, m); -} - -QDebug operator<<(QDebug dbg, MembershipMask mm) -{ - return suppressScopeAndDump(dbg, mm) << ")"; -} - -QDebug operator<<(QDebug dbg, JoinState js) -{ - return suppressScopeAndDump(dbg, js); -} - -QDebug operator<<(QDebug dbg, JoinStates jss) -{ - return suppressScopeAndDump(dbg, jss) << ")"; -} diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 3d8ace67..969ebe90 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -118,9 +118,3 @@ constexpr inline auto RoomTypeStrings = make_array( } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) - -class QDebug; -QDebug operator<<(QDebug dbg, Quotient::Membership m); -QDebug operator<<(QDebug dbg, Quotient::MembershipMask m); -QDebug operator<<(QDebug dbg, Quotient::JoinState js); -QDebug operator<<(QDebug dbg, Quotient::JoinStates js); 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 2d1cf137d7380a15673826bce00e71461fbc7446 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 19 Nov 2021 12:46:00 +0100 Subject: Cleanup --- lib/events/roommemberevent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 469dbb32..b0bc7bcb 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -17,7 +17,7 @@ struct JsonConverter { const auto& ms = jv.toString(); if (ms.isEmpty()) { - qCWarning(EVENTS) << "Empty member state:" << ms; + qCWarning(EVENTS) << "Empty membership state"; return Membership::Invalid; } const auto it = -- cgit v1.2.3 From 71b7f05e42f93c3da590b6f7f55658a81b607c0e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 19 Nov 2021 17:17:30 +0100 Subject: Add continue-on-error for regenerated API files The current upstream API definitions are expected to fail the test. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47e31d55..f22ea6d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ >>$GITHUB_ENV - echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV - name: Initialize CodeQL tools if: env.CODEQL_ANALYSIS @@ -154,6 +154,7 @@ jobs: ls ~/.local/$BIN_DIR/quotest - name: Run tests + continue-on-error: ${{ matrix.update-api != '' }} # the current upstream API definitions are expected to fail the test env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} -- cgit v1.2.3 From 02fa42b8ac5694485c7f6dedc62a873e97da2d35 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 19 Nov 2021 18:57:36 +0100 Subject: continue-on-error on the job level Continue on the step level marks the whole job as successful which is not really accurate. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f22ea6d2..269e487c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ defaults: jobs: CI: runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.update-api != '' }} # the current upstream API definitions are expected to fail the test strategy: fail-fast: false max-parallel: 1 @@ -154,7 +155,6 @@ jobs: ls ~/.local/$BIN_DIR/quotest - name: Run tests - continue-on-error: ${{ matrix.update-api != '' }} # the current upstream API definitions are expected to fail the test env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} -- 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 ++++++++++++++++++++++++++++++------------- lib/room.h | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 13 deletions(-) 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; diff --git a/lib/room.h b/lib/room.h index 8a544e82..124c8cb4 100644 --- a/lib/room.h +++ b/lib/room.h @@ -96,6 +96,18 @@ inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) swap(lhs.timestamp, rhs.timestamp); } +struct Notification +{ + enum Type { None = 0, Basic, Highlight }; + Q_ENUM(Notification) + + Type type = None; + +private: + Q_GADGET + Q_PROPERTY(Type type MEMBER type CONSTANT) +}; + class Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) @@ -482,6 +494,29 @@ public: //! \sa markAllMessagesAsRead, fullyReadMarker Q_INVOKABLE void markMessagesAsRead(QString uptoEventId); + //! \brief Determine whether an event should be counted as unread + //! + //! The criteria of including an event in unread counters are described in + //! [MSC2654](https://github.com/matrix-org/matrix-doc/pull/2654); according + //! to these, the event should be counted as unread (or, in libQuotient + //! parlance, is "notable") if it is: + //! - either + //! - a message event that is not m.notice, or + //! - a state event with type being one of: + //! `m.room.topic`, `m.room.name`, `m.room.avatar`, `m.room.tombstone`; + //! - neither redacted, nor an edit (redactions cause the redacted event + //! to stop being notable, while edits are not notable themselves while + //! the original event usually is); + //! - from a non-local user (events from other devices of the local + //! user are not notable). + virtual bool isEventNotable(const TimelineItem& ti) const; + + //! \brief Get notification details for an event + //! + //! This allows to get details on the kind of notification that should + //! generated for \p evt. + Notification notificationFor(const TimelineItem& ti) const; + //! Check whether there are unread messages in the room bool hasUnreadMessages() const; @@ -873,6 +908,7 @@ protected: {} virtual QJsonObject toJson() const; virtual void updateData(SyncRoomData&& data, bool fromCache = false); + virtual Notification checkForNotifications(const TimelineItem& ti); private: friend class Connection; -- 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 ++++++++++++++++----------------- lib/room.h | 24 ++++++++++++++++++------ lib/syncdata.cpp | 44 ++++++++++++++++++++++---------------------- lib/syncdata.h | 15 +++++++++------ 4 files changed, 65 insertions(+), 51 deletions(-) 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" diff --git a/lib/room.h b/lib/room.h index 124c8cb4..a75311fb 100644 --- a/lib/room.h +++ b/lib/room.h @@ -149,10 +149,10 @@ class Room : public QObject { Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged STORED false) Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) - Q_PROPERTY(int highlightCount READ highlightCount NOTIFY - highlightCountChanged RESET resetHighlightCount) - Q_PROPERTY(int notificationCount READ notificationCount NOTIFY - notificationCountChanged RESET resetNotificationCount) + Q_PROPERTY(qsizetype highlightCount READ highlightCount + NOTIFY highlightCountChanged) + Q_PROPERTY(qsizetype notificationCount READ notificationCount + NOTIFY notificationCountChanged) Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages STORED false) Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) @@ -538,9 +538,21 @@ public: */ int unreadCount() const; - Q_INVOKABLE int notificationCount() const; + //! \brief Get the number of notifications since the last read receipt + //! + //! \sa lastLocalReadReceipt + qsizetype notificationCount() const; + + //! \deprecated Use setReadReceipt() to drive changes in notification count Q_INVOKABLE void resetNotificationCount(); - Q_INVOKABLE int highlightCount() const; + + //! \brief Get the number of highlights since the last read receipt + //! + //! As of 0.7, this is defined by the homeserver as Quotient doesn't process + //! push rules. + qsizetype highlightCount() const; + + //! \deprecated Use setReadReceipt() to drive changes in highlightCount Q_INVOKABLE void resetHighlightCount(); /** Check whether the room has account data of the given type diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index e86d3100..396e77eb 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -10,9 +10,6 @@ using namespace Quotient; -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-quotient.unread_count"); - bool RoomSummary::isEmpty() const { return !joinedMemberCount && !invitedMemberCount && !heroes; @@ -64,23 +61,23 @@ inline EventsArrayT load(const QJsonObject& batches, StrT keyName) return fromJson(batches[keyName].toObject().value("events"_ls)); } -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , summary(fromJson(room_["summary"_ls])) - , state(load(room_, joinState == JoinState::Invite +SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState, + const QJsonObject& roomJson) + : roomId(std::move(roomId_)) + , joinState(joinState) + , summary(fromJson(roomJson["summary"_ls])) + , state(load(roomJson, joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) { switch (joinState) { case JoinState::Join: - ephemeral = load(room_, "ephemeral"_ls); + ephemeral = load(roomJson, "ephemeral"_ls); [[fallthrough]]; case JoinState::Leave: { - accountData = load(room_, "account_data"_ls); - timeline = load(room_, "timeline"_ls); - const auto timelineJson = room_.value("timeline"_ls).toObject(); + accountData = load(roomJson, "account_data"_ls); + timeline = load(roomJson, "timeline"_ls); + const auto timelineJson = roomJson.value("timeline"_ls).toObject(); timelineLimited = timelineJson.value("limited"_ls).toBool(); timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); @@ -89,14 +86,17 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, default: /* nothing on top of state */; } - const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - fromJson(unreadJson.value(UnreadCountKey), unreadCount); - fromJson(unreadJson.value("highlight_count"_ls), highlightCount); - fromJson(unreadJson.value("notification_count"_ls), notificationCount); - if (highlightCount.has_value() || notificationCount.has_value()) - qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << *highlightCount - << "and notifications:" << *notificationCount; + const auto unreadJson = roomJson.value(UnreadNotificationsKey).toObject(); + + fromJson(unreadJson.value(PartiallyReadCountKey), partiallyReadCount); + if (!partiallyReadCount.has_value()) + fromJson(unreadJson.value("x-quotient.unread_count"_ls), + partiallyReadCount); + + fromJson(roomJson.value(NewUnreadCountKey), unreadCount); + if (!unreadCount.has_value()) + fromJson(unreadJson.value("notification_count"_ls), unreadCount); + fromJson(unreadJson.value(HighlightCountKey), highlightCount); } SyncData::SyncData(const QString& cacheFileName) @@ -130,7 +130,7 @@ Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } std::pair SyncData::cacheVersion() { - return { MajorCacheVersion, 1 }; + return { MajorCacheVersion, 2 }; } QJsonObject SyncData::loadJson(const QString& fileName) diff --git a/lib/syncdata.h b/lib/syncdata.h index b869a541..36d2e0bf 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -8,6 +8,12 @@ #include "events/stateevent.h" namespace Quotient { + +constexpr auto UnreadNotificationsKey = "unread_notifications"_ls; +constexpr auto PartiallyReadCountKey = "x-quotient.since_fully_read_count"_ls; +constexpr auto NewUnreadCountKey = "org.matrix.msc2654.unread_count"_ls; +constexpr auto HighlightCountKey = "highlight_count"_ls; + /// Room summary, as defined in MSC688 /** * Every member of this structure is an Omittable; as per the MSC, only @@ -29,7 +35,6 @@ struct RoomSummary { }; QDebug operator<<(QDebug dbg, const RoomSummary& rs); - template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RoomSummary& rs); @@ -48,16 +53,14 @@ public: bool timelineLimited; QString timelinePrevBatch; + Omittable partiallyReadCount; Omittable unreadCount; Omittable highlightCount; - Omittable notificationCount; - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); + SyncRoomData(QString roomId, JoinState joinState, + const QJsonObject& roomJson); SyncRoomData(SyncRoomData&&) = default; SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; }; // QVector cannot work with non-copyable objects, std::vector can. -- 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 +++++++++++++++++++++++++++++++++++++----------------------- lib/room.h | 2 +- 2 files changed, 54 insertions(+), 33 deletions(-) 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) diff --git a/lib/room.h b/lib/room.h index a75311fb..c228f6c9 100644 --- a/lib/room.h +++ b/lib/room.h @@ -492,7 +492,7 @@ public: //! the current m.fully_read marker or is not loaded, to prevent //! accidentally trying to move the marker back in the timeline. //! \sa markAllMessagesAsRead, fullyReadMarker - Q_INVOKABLE void markMessagesAsRead(QString uptoEventId); + Q_INVOKABLE void markMessagesAsRead(const QString& uptoEventId); //! \brief Determine whether an event should be counted as unread //! -- 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. --- CMakeLists.txt | 1 + lib/eventstats.cpp | 98 ++++++++++++++ lib/eventstats.h | 107 +++++++++++++++ lib/room.cpp | 386 ++++++++++++++++++++++++++++++++++------------------- lib/room.h | 116 ++++++++++++---- 5 files changed, 544 insertions(+), 164 deletions(-) create mode 100644 lib/eventstats.cpp create mode 100644 lib/eventstats.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3814bc7e..eaf662cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,7 @@ list(APPEND lib_SRCS lib/avatar.cpp lib/uri.cpp lib/uriresolver.cpp + lib/eventstats.cpp lib/syncdata.cpp lib/settings.cpp lib/networksettings.cpp diff --git a/lib/eventstats.cpp b/lib/eventstats.cpp new file mode 100644 index 00000000..9fa7f5ff --- /dev/null +++ b/lib/eventstats.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2021 Quotient contributors +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "eventstats.h" + +using namespace Quotient; + +EventStats EventStats::fromRange(const Room* room, const Room::rev_iter_t& from, + const Room::rev_iter_t& to, + const EventStats& init) +{ + Q_ASSERT(to <= room->historyEdge()); + Q_ASSERT(from >= Room::rev_iter_t(room->syncEdge())); + Q_ASSERT(from <= to); + QElapsedTimer et; + et.start(); + const auto result = + accumulate(from, to, init, + [room](EventStats acc, const TimelineItem& ti) { + acc.notableCount += room->isEventNotable(ti); + acc.highlightCount += room->notificationFor(ti).type + == Notification::Highlight; + return acc; + }); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(PROFILER).nospace() + << "Event statistics collection over index range [" << from->index() + << "," << (to - 1)->index() << "] took " << et; + return result; +} + +EventStats EventStats::fromMarker(const Room* room, + const EventStats::marker_t& marker) +{ + const auto s = fromRange(room, marker_t(room->syncEdge()), marker, + { 0, 0, marker == room->historyEdge() }); + Q_ASSERT(s.isValidFor(room, marker)); + return s; +} + +EventStats EventStats::fromCachedCounters(Omittable notableCount, + Omittable highlightCount) +{ + const auto hCount = std::max(0, highlightCount.value_or(0)); + if (!notableCount.has_value()) + return { 0, hCount, true }; + auto nCount = notableCount.value_or(0); + return { std::max(0, nCount), hCount, nCount != -1 }; +} + +bool EventStats::updateOnMarkerMove(const Room* room, const marker_t& oldMarker, + const marker_t& newMarker) +{ + if (newMarker == oldMarker) + return false; + + // Double-check consistency between the old marker and the old stats + Q_ASSERT(isValidFor(room, oldMarker)); + Q_ASSERT(oldMarker > newMarker); + + // A bit of optimisation: only calculate the difference if the marker moved + // less than half the remaining timeline ahead; otherwise, recalculation + // over the remaining timeline will very likely be faster. + if (oldMarker != room->historyEdge() + && oldMarker - newMarker < newMarker - marker_t(room->syncEdge())) { + const auto removedStats = fromRange(room, newMarker, oldMarker); + Q_ASSERT(notableCount >= removedStats.notableCount + && highlightCount >= removedStats.highlightCount); + notableCount -= removedStats.notableCount; + highlightCount -= removedStats.highlightCount; + return removedStats.notableCount > 0 || removedStats.highlightCount > 0; + } + + const auto newStats = EventStats::fromMarker(room, newMarker); + if (!isEstimate && newStats == *this) + return false; + *this = newStats; + return true; +} + +bool EventStats::isValidFor(const Room* room, const marker_t& marker) const +{ + const auto markerAtHistoryEdge = marker == room->historyEdge(); + // Either markerAtHistoryEdge and isEstimate are in the same state, or it's + // a special case of no notable events and the marker at history edge + // (then isEstimate can assume any value). + return markerAtHistoryEdge == isEstimate + || (markerAtHistoryEdge && notableCount == 0); +} + +QDebug Quotient::operator<<(QDebug dbg, const EventStats& es) +{ + QDebugStateSaver _(dbg); + dbg.nospace() << es.notableCount << '/' << es.highlightCount; + if (es.isEstimate) + dbg << " (estimated)"; + return dbg; +} diff --git a/lib/eventstats.h b/lib/eventstats.h new file mode 100644 index 00000000..9be83377 --- /dev/null +++ b/lib/eventstats.h @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2021 Quotient contributors +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "room.h" + +namespace Quotient { + +//! \brief Counters of unread events and highlights with a precision flag +//! +//! This structure contains a static snapshot with values of unread counters +//! returned by Room::partiallyReadStats and Room::unreadStats (properties +//! or methods). +//! +//! \note It's just a simple grouping of counters and is not automatically +//! updated from the room as subsequent syncs arrive. +//! \sa Room::unreadStats, Room::partiallyReadStats, Room::isEventNotable +struct EventStats { + Q_GADGET + Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT) + Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT) + Q_PROPERTY(bool isEstimate MEMBER isEstimate CONSTANT) +public: + //! The number of "notable" events in an events range + //! \sa Room::isEventNotable + qsizetype notableCount = 0; + qsizetype highlightCount = 0; + //! \brief Whether the counter values above are exact + //! + //! This is false when the end marker (m.read receipt or m.fully_read) used + //! to collect the stats points to an event loaded locally and the counters + //! can therefore be calculated exactly using the locally available segment + //! of the timeline; true when the marker points to an event outside of + //! the local timeline (in which case the estimation is made basing on + //! the data supplied by the homeserver as well as counters saved from + //! the previous run of the client). + bool isEstimate = true; + + bool operator==(const EventStats& rhs) const& = default; + + //! \brief Check whether the event statistics are empty + //! + //! Empty statistics have notable and highlight counters of zero and + //! isEstimate set to false. + Q_INVOKABLE bool empty() const + { + return notableCount == 0 && !isEstimate && highlightCount == 0; + } + + using marker_t = Room::rev_iter_t; + + //! \brief Build event statistics on a range of events + //! + //! This is a factory that returns an EventStats instance with counts of + //! notable and highlighted events between \p from and \p to reverse + //! timeline iterators; the \p init parameter allows to override + //! the initial statistics object and start from other values. + static EventStats fromRange(const Room* room, const marker_t& from, + const marker_t& to, + const EventStats& init = { 0, 0, false }); + + //! \brief Build event statistics on a range from sync edge to marker + //! + //! This is mainly a shortcut for \code + //! fromRange(room, marker_t(room->syncEdge()), marker) + //! \endcode except that it also sets isEstimate to true if (and only if) + //! to == room->historyEdge(). + static EventStats fromMarker(const Room* room, const marker_t& marker); + + //! \brief Loads a statistics object from the cached counters + //! + //! Sets isEstimate to `true` unless both notableCount and highlightCount + //! are equal to -1. + static EventStats fromCachedCounters(Omittable notableCount, + Omittable highlightCount = none); + + //! \brief Update statistics when a read marker moves down the timeline + //! + //! Removes events between oldMarker and newMarker from statistics + //! calculation if \p oldMarker points to an existing event in the timeline, + //! or recalculates the statistics entirely if \p oldMarker points + //! to room->historyEdge(). Always results in exact statistics + //! (isEstimate == false. + //! \param oldMarker Must point correspond to the _current_ statistics + //! isEstimate state, i.e. it should point to + //! room->historyEdge() if isEstimate == true, or + //! to a valid position within the timeline otherwise + //! \param newMarker Must point to a valid position in the timeline (not to + //! room->historyEdge() that is equal to or closer to + //! the sync edge than \p oldMarker + //! \return true if either notableCount or highlightCount changed, or if + //! the statistics was completely recalculated; false otherwise + bool updateOnMarkerMove(const Room* room, const marker_t& oldMarker, + const marker_t& newMarker); + + //! \brief Validate the statistics object against the given marker + //! + //! Checks whether the statistics object data are valid for a given marker. + //! No stats recalculation takes place, only isEstimate and zero-ness + //! of notableCount are checked. + bool isValidFor(const Room* room, const marker_t& marker) const; +}; + +QDebug operator<<(QDebug dbg, const EventStats& es); + +} 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" diff --git a/lib/room.h b/lib/room.h index c228f6c9..2f46e3a8 100644 --- a/lib/room.h +++ b/lib/room.h @@ -79,7 +79,7 @@ class ReadReceipt { Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT) public: QString eventId; - QDateTime timestamp; + QDateTime timestamp = {}; bool operator==(const ReadReceipt& other) const { @@ -96,6 +96,8 @@ inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) swap(lhs.timestamp, rhs.timestamp); } +struct EventStats; + struct Notification { enum Type { None = 0, Basic, Highlight }; @@ -146,13 +148,18 @@ class Room : public QObject { markMessagesAsRead NOTIFY readMarkerMoved) Q_PROPERTY(QString lastFullyReadEventId READ lastFullyReadEventId WRITE markMessagesAsRead NOTIFY fullyReadMarkerMoved) + //! \deprecated since 0.7 Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY - unreadMessagesChanged STORED false) - Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) + partiallyReadStatsChanged STORED false) + //! \deprecated since 0.7 + Q_PROPERTY(int unreadCount READ unreadCount NOTIFY partiallyReadStatsChanged + STORED false) Q_PROPERTY(qsizetype highlightCount READ highlightCount NOTIFY highlightCountChanged) Q_PROPERTY(qsizetype notificationCount READ notificationCount NOTIFY notificationCountChanged) + Q_PROPERTY(EventStats partiallyReadStats READ partiallyReadStats NOTIFY partiallyReadStatsChanged) + Q_PROPERTY(EventStats unreadStats READ unreadStats NOTIFY unreadStatsChanged) Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages STORED false) Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) @@ -175,12 +182,13 @@ public: Aliases = 0x2, CanonicalAlias = Aliases, Topic = 0x4, - UnreadNotifs = 0x8, + PartiallyReadStats = 0x8, + DECL_DEPRECATED_ENUMERATOR(UnreadNotifs, PartiallyReadStats), Avatar = 0x10, JoinState = 0x20, Tags = 0x40, Members = 0x80, - /* = 0x100, */ + UnreadStats = 0x100, AccountData Q_DECL_ENUMERATOR_DEPRECATED_X( "Change::AccountData will be merged into Change::Other in 0.8") = 0x200, @@ -188,6 +196,7 @@ public: ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X( "Change::ReadMarker will be merged into Change::Other in 0.8") = 0x800, + Highlights = 0x1000, Other = 0x8000, Any = 0xFFFF }; @@ -509,6 +518,7 @@ public: //! the original event usually is); //! - from a non-local user (events from other devices of the local //! user are not notable). + //! \sa partiallyReadStats, unreadStats virtual bool isEventNotable(const TimelineItem& ti) const; //! \brief Get notification details for an event @@ -517,30 +527,84 @@ public: //! generated for \p evt. Notification notificationFor(const TimelineItem& ti) const; - //! Check whether there are unread messages in the room + //! \brief Get event statistics since the fully read marker + //! + //! This call returns a structure containing: + //! - the number of notable unread events since the fully read marker; + //! depending on the fully read marker state with respect to the local + //! timeline, this number may be either exact or estimated + //! (see EventStats::isEstimate); + //! - the number of highlights (TODO). + //! + //! Note that this is different from the unread count defined by MSC2654 + //! and from the notification/highlight numbers defined by the spec in that + //! it counts events since the fully read marker, not since the last + //! read receipt position. + //! + //! As E2EE is not supported in the library, the returned result will always + //! be an estimate (isEstimate == true) for encrypted rooms; + //! moreover, since the library doesn't know how to tackle push rules yet + //! the number of highlights returned here will always be zero (there's no + //! good substitute for that now). + //! + //! \sa isEventNotable, fullyReadMarker, unreadStats, EventStats + EventStats partiallyReadStats() const; + + //! \brief Get event statistics since the last read receipt + //! + //! This call returns a structure that contains the following three numbers, + //! all counted on the timeline segment between the event pointed to by + //! the m.fully_read marker and the sync edge: + //! - the number of unread events - depending on the read receipt state + //! with respect to the local timeline, this number may be either precise + //! or estimated (see EventStats::isEstimate); + //! - the number of highlights (TODO). + //! + //! As E2EE is not supported in the library, the returned result will always + //! be an estimate (isEstimate == true) for encrypted rooms; + //! moreover, since the library doesn't know how to tackle push rules yet + //! the number of highlights returned here will always be zero - use + //! highlightCount() for now. + //! + //! \sa isEventNotable, lastLocalReadReceipt, partiallyReadStats, + //! highlightCount + EventStats unreadStats() const; + + [[deprecated( + "Use partiallyReadStats/unreadStats() and EventStats::empty()")]] bool hasUnreadMessages() const; - /** Get the number of unread messages in the room - * Depending on the read marker state, this call may return either - * a precise or an estimate number of unread events. Only "notable" - * events (non-redacted message events from users other than local) - * are counted. - * - * In a case when readMarker() == historyEdge() (the local read - * marker is beyond the local timeline) only the bottom limit of - * the unread messages number can be estimated (and even that may - * be slightly off due to, e.g., redactions of events not loaded - * to the local timeline). - * - * If all messages are read, this function will return -1 (_not_ 0, - * as zero may mean "zero or more unread messages" in a situation - * when the read marker is outside the local timeline. - */ + //! \brief Get the number of notable events since the fully read marker + //! + //! \deprecated Since 0.7 there are two ways to count unread events: since + //! the fully read marker (used by libQuotient pre-0.7) and since the last + //! read receipt (as used by most of Matrix ecosystem, including the spec + //! and MSCs). This function currently returns a value derived from + //! partiallyReadStats() for compatibility with libQuotient 0.6; it will be + //! removed due to ambiguity. Use unreadStats() to obtain the spec-compliant + //! count of unread events and the highlight count; partiallyReadStats() to + //! obtain the unread events count since the fully read marker. + //! + //! \return -1 (_not 0_) when all messages are known to have been fully read, + //! i.e. the fully read marker points to _the latest notable_ event + //! loaded in the local timeline (which may be different from + //! the latest event in the local timeline as that might not be + //! notable); + //! 0 when there may be unread messages but the current local + //! timeline doesn't have any notable ones (often but not always + //! because it's entirely empty yet); + //! a positive integer when there is (or estimated to be) a number + //! of unread notable events as described above. + //! + //! \sa partiallyReadStats, unreadStats + [[deprecated("Use partiallyReadStats() or unreadStats() instead")]] // int unreadCount() const; //! \brief Get the number of notifications since the last read receipt //! - //! \sa lastLocalReadReceipt + //! This is the same as unreadStats().notableCount. + //! + //! \sa unreadStats, lastLocalReadReceipt qsizetype notificationCount() const; //! \deprecated Use setReadReceipt() to drive changes in notification count @@ -550,6 +614,8 @@ public: //! //! As of 0.7, this is defined by the homeserver as Quotient doesn't process //! push rules. + //! + //! \sa unreadStats, lastLocalReadReceipt qsizetype highlightCount() const; //! \deprecated Use setReadReceipt() to drive changes in highlightCount @@ -878,7 +944,11 @@ Q_SIGNALS: //! \deprecated since 0.7 - use lastReadEventChanged void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use either partiallyReadStatsChanged + //! or unreadStatsChanged void unreadMessagesChanged(Quotient::Room* room); + void partiallyReadStatsChanged(); + void unreadStatsChanged(); void accountDataAboutToChange(QString type); void accountDataChanged(QString type); -- cgit v1.2.3 From 9e5c9ff3ef0eeff8abd0bc3508d9152506e27e30 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 20 Nov 2021 11:42:53 +0100 Subject: Document Room::Change and its enumerators --- lib/room.h | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/room.h b/lib/room.h index 2f46e3a8..706fb17f 100644 --- a/lib/room.h +++ b/lib/room.h @@ -176,28 +176,45 @@ public: using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; + //! \brief Room changes that can be tracked using Room::changed() signal + //! + //! This enumeration lists kinds of changes that can be tracked with + //! a "cumulative" changed() signal instead of using individual signals for + //! each change. Specific enumerators mention these individual signals. + //! \sa changed enum class Change : uint { - None = 0x0, - Name = 0x1, - Aliases = 0x2, + None = 0x0, //< No changes occurred in the room + Name = 0x1, //< \sa namesChanged, displaynameChanged + Aliases = 0x2, //< \sa namesChanged, displaynameChanged CanonicalAlias = Aliases, - Topic = 0x4, - PartiallyReadStats = 0x8, + Topic = 0x4, //< \sa topicChanged + PartiallyReadStats = 0x8, //< \sa partiallyReadStatsChanged DECL_DEPRECATED_ENUMERATOR(UnreadNotifs, PartiallyReadStats), - Avatar = 0x10, - JoinState = 0x20, - Tags = 0x40, + Avatar = 0x10, //< \sa avatarChanged + JoinState = 0x20, //< \sa joinStateChanged + Tags = 0x40, //< \sa tagsChanged + //! \sa userAdded, userRemoved, memberRenamed, memberListChanged, + //! displaynameChanged Members = 0x80, - UnreadStats = 0x100, + UnreadStats = 0x100, //< \sa unreadStatsChanged AccountData Q_DECL_ENUMERATOR_DEPRECATED_X( "Change::AccountData will be merged into Change::Other in 0.8") = 0x200, - Summary = 0x400, + Summary = 0x400, //< \sa summaryChanged, displaynameChanged ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X( "Change::ReadMarker will be merged into Change::Other in 0.8") = 0x800, - Highlights = 0x1000, + Highlights = 0x1000, //< \sa highlightCountChanged + //! A catch-all value that covers changes not listed above (such as + //! encryption turned on or the room having been upgraded), as well as + //! changes in the room state that the library is not aware of (e.g., + //! custom state events) and m.read/m.fully_read position changes. + //! \sa encryptionChanged, upgraded, accountDataChanged Other = 0x8000, + //! This is intended to test a Change/Changes value for non-emptiness; + //! testFlag(Change::Any) or adding & Change::Any has + //! the same meaning as !testFlag(Change::None) or adding + //! != Change::None. Any = 0xFFFF }; QUO_DECLARE_FLAGS(Changes, Change) @@ -931,12 +948,14 @@ Q_SIGNALS: Quotient::JoinState newState); void typingChanged(); - void highlightCountChanged(); - void notificationCountChanged(); + void highlightCountChanged(); //< \sa highlightCount + void notificationCountChanged(); //< \sa notificationCount void displayedChanged(bool displayed); void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); + //! The event that m.read receipt points to has changed + //! \sa lastReadReceipt void lastReadEventChanged(Quotient::User* user); void fullyReadMarkerMoved(QString fromEventId, QString toEventId); //! \deprecated since 0.7 - use fullyReadMarkerMoved -- cgit v1.2.3 From 52e640bce5a8931330fa6d653212e524e7baa2eb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 20 Nov 2021 11:42:21 +0100 Subject: Minor brushup in the comment to QUO_DECLARE_FLAGS [skip ci] --- lib/quotient_common.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 969ebe90..d91c3d17 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -9,10 +9,10 @@ // See https://bugreports.qt.io/browse/QTBUG-82295 - despite the comment that // Q_FLAG[_NS] "should" be applied to the enum only, Qt doesn't allow to wrap -// a flag type into a QVariant then. The macros below define Q_FLAG_NS and on -// top of that add a part of Q_ENUM() that enables the metatype data but goes -// under the moc radar to avoid double registration of the same data in the map -// defined in moc_*.cpp +// a flag type into a QVariant then. The macros below define Q_FLAG[_NS] and on +// top of that add Q_ENUM[_NS]_IMPL which is a part of Q_ENUM() macro that +// enables the metatype data but goes under the moc radar to avoid double +// registration of the same data in the map defined in moc_*.cpp #define QUO_DECLARE_FLAGS(Flags, Enum) \ Q_DECLARE_FLAGS(Flags, Enum) \ Q_ENUM_IMPL(Enum) \ -- 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(-) 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 e16ed41a6a28a43919126a85f7a802c5b2e091b6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 15:29:57 +0100 Subject: CI: Nicer and more detailed logging for quotest --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 269e487c..dd862a0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,6 +158,8 @@ jobs: env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} + QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' + QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From a2cc707107464fd98fc8a33afde3ed29f8cd9526 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 21 Nov 2021 21:48:54 +0100 Subject: CI: Restrict workflow concurrency; tighten job timeout --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 269e487c..1b6d9e52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ defaults: run: shell: bash +concurrency: ci-${{ github.ref }} + jobs: CI: runs-on: ${{ matrix.os }} @@ -160,7 +162,7 @@ jobs: TEST_PWD: ${{ secrets.TEST_PWD }} run: | [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" - timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually + timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually - name: Perform CodeQL analysis if: env.CODEQL_ANALYSIS -- 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(-) 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(-) 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 c57d6de40fb790a4920a9c8ff235511860d68f32 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 24 Nov 2021 09:50:34 +0100 Subject: Don't require C++20 from the clients just yet --- lib/eventstats.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/eventstats.h b/lib/eventstats.h index 9be83377..77c661a7 100644 --- a/lib/eventstats.h +++ b/lib/eventstats.h @@ -37,7 +37,14 @@ public: //! the previous run of the client). bool isEstimate = true; - bool operator==(const EventStats& rhs) const& = default; + // TODO: replace with = default once C++20 becomes a requirement on clients + bool operator==(const EventStats& rhs) const + { + return notableCount == rhs.notableCount + && highlightCount == rhs.highlightCount + && isEstimate == rhs.isEstimate; + } + bool operator!=(const EventStats& rhs) const { return !operator==(rhs); } //! \brief Check whether the event statistics are empty //! -- cgit v1.2.3 From f6155d62740a88b020273ba623c816f7b9805772 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 26 Nov 2021 13:44:39 +0100 Subject: Drop Q_GADGET from most uncopyable classes; other minor cleanup Q_GADGET is generally used to enable two things outside of QObject: Q_PROPERTY/Q_INVOKABLE and Q_ENUM/Q_FLAG. While the latter can be used in its own right in QML, the former requires Q_GADGET instances to be passed to QML by value, which is not really possible with uncopyable/unassignable classes. Bottom line is that Q_PROPERTY in anything derived from Quotient::Event is not viable, making Q_GADGET macro useless unless there's a Q_ENUM/Q_FLAG (as is the case with RoomMessageEvent, e.g.). --- lib/eventitem.h | 5 +---- lib/events/encryptedevent.h | 2 -- lib/events/event.h | 3 --- lib/events/roomevent.h | 41 +++++++++++++---------------------------- lib/events/roommessageevent.h | 4 ---- lib/events/stateevent.h | 2 -- lib/quotient_common.h | 1 - 7 files changed, 14 insertions(+), 44 deletions(-) diff --git a/lib/eventitem.h b/lib/eventitem.h index a70a3c3e..0ab1a01d 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -9,7 +9,6 @@ #include namespace Quotient { -class StateEventBase; namespace EventStatus { Q_NAMESPACE @@ -31,8 +30,7 @@ namespace EventStatus { Replaced = 0x10, //< The event has been replaced Hidden = 0x100, //< The event should not be shown in the timeline }; - Q_DECLARE_FLAGS(Status, Code) - Q_FLAG_NS(Status) + Q_ENUM_NS(Code) } // namespace EventStatus class EventItemBase { @@ -106,7 +104,6 @@ inline const CallEventBase* EventItemBase::viewAs() const } class PendingEventItem : public EventItemBase { - Q_GADGET public: using EventItemBase::EventItemBase; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index eb7123eb..598829cd 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -7,7 +7,6 @@ #include "roomevent.h" namespace Quotient { -class Room; /* * While the specification states: * @@ -27,7 +26,6 @@ class Room; * one and doesn't add new restrictions, just provides additional features. */ class EncryptedEvent : public RoomEvent { - Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) diff --git a/lib/events/event.h b/lib/events/event.h index 78853ced..2ed5de5d 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -195,9 +195,6 @@ inline auto registerEventType() // === Event === class Event { - Q_GADGET - Q_PROPERTY(Type type READ type CONSTANT) - Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) public: using Type = event_type_t; using factory_t = EventFactory; diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 3174764f..a6cd84ca 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -12,16 +12,6 @@ class RedactionEvent; /** This class corresponds to m.room.* events */ class RoomEvent : public Event { - Q_GADGET - Q_PROPERTY(QString id READ id) - //! \deprecated Use originTimestamp instead - Q_PROPERTY(QDateTime timestamp READ originTimestamp CONSTANT) - Q_PROPERTY(QDateTime originTimestamp READ originTimestamp CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) - Q_PROPERTY(QString senderId READ senderId CONSTANT) - Q_PROPERTY(QString redactionReason READ redactionReason) - Q_PROPERTY(bool isRedacted READ isRedacted) - Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId) public: using factory_t = EventFactory; @@ -51,28 +41,23 @@ public: QString transactionId() const; QString stateKey() const; + //! \brief Fill the pending event object with the room id void setRoomId(const QString& roomId); + //! \brief Fill the pending event object with the sender id void setSender(const QString& senderId); - - /** - * Sets the transaction id for locally created events. This should be - * done before the event is exposed to any code using the respective - * Q_PROPERTY. - * - * \param txnId - transaction id, normally obtained from - * Connection::generateTxnId() - */ + //! \brief Fill the pending event object with the transaction id + //! \param txnId - transaction id, normally obtained from + //! Connection::generateTxnId() void setTransactionId(const QString& txnId); - /** - * Sets event id for locally created events - * - * When a new event is created locally, it has no server id yet. - * This function allows to add the id once the confirmation from - * the server is received. There should be no id set previously - * in the event. It's the responsibility of the code calling addId() - * to notify clients that use Q_PROPERTY(id) about its change - */ + //! \brief Add an event id to locally created events after they are sent + //! + //! When a new event is created locally, it has no id; the homeserver + //! assigns it once the event is sent. This function allows to add the id + //! once the confirmation from the server is received. There should be no id + //! set previously in the event. It's the responsibility of the code calling + //! addId() to notify clients about the change; there's no signal or + //! callback for that in RoomEvent. void addId(const QString& newId); protected: diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 88d3b74c..56597ddc 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -18,10 +18,6 @@ namespace MessageEventContent = EventContent; // Back-compatibility */ class RoomMessageEvent : public RoomEvent { Q_GADGET - Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT) - Q_PROPERTY(QString plainBody READ plainBody CONSTANT) - Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT) - Q_PROPERTY(const EventContent::TypedBase* content READ content CONSTANT) public: DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index bc414a5f..b0aa9907 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -18,8 +18,6 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, } class StateEventBase : public RoomEvent { - Q_GADGET - Q_PROPERTY(QString stateKey READ stateKey CONSTANT) public: using factory_t = EventFactory; diff --git a/lib/quotient_common.h b/lib/quotient_common.h index d91c3d17..0e3e2a40 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -90,7 +90,6 @@ constexpr inline auto JoinStateStrings = make_array( //! So far only background/foreground flags are available. //! \sa Connection::callApi, Connection::run enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; - Q_ENUM_NS(RunningPolicy) //! \brief The result of URI resolution using UriResolver -- cgit v1.2.3 From f4a0acf818c4c89d132b2ec96d47c5817b106149 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 26 Nov 2021 13:46:58 +0100 Subject: Drop #include "logging.h" from event.h Makes compilation a tad lighter. --- lib/events/callanswerevent.cpp | 1 + lib/events/callhangupevent.cpp | 1 + lib/events/callinviteevent.cpp | 1 + lib/events/encryptedevent.cpp | 11 ++++++++--- lib/events/encryptedevent.h | 14 ++++---------- lib/events/encryptionevent.cpp | 1 + lib/events/event.cpp | 11 ++++++++--- lib/events/event.h | 5 +++-- lib/events/reactionevent.cpp | 1 + lib/events/roomcreateevent.cpp | 1 + lib/events/roomkeyevent.cpp | 1 + lib/syncdata.cpp | 1 + 12 files changed, 31 insertions(+), 18 deletions(-) diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index be83d9d0..24144585 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "callanswerevent.h" +#include "logging.h" /* m.call.answer diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp index 43bc4db0..537ace75 100644 --- a/lib/events/callhangupevent.cpp +++ b/lib/events/callhangupevent.cpp @@ -6,6 +6,7 @@ */ #include "callhangupevent.h" +#include "logging.h" /* m.call.hangup diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 5ea54662..ce4fb101 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "callinviteevent.h" +#include "logging.h" /* m.call.invite diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 0290f973..ebf733ac 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" +#include "logging.h" using namespace Quotient; @@ -25,8 +26,12 @@ EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, }) {} -EncryptedEvent::EncryptedEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) +QString EncryptedEvent::algorithm() const { - qCDebug(E2EE) << "Encrypted event from" << senderId(); + const auto algo = content(AlgorithmKeyL); + if (!SupportedAlgorithms.contains(algo)) { + qCWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + } + return algo; } diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 598829cd..d2b71785 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -37,17 +37,11 @@ public: /* In case with Megolm, device_id and session_id are required */ explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId); - explicit EncryptedEvent(const QJsonObject& obj); + explicit EncryptedEvent(const QJsonObject& obj) + : RoomEvent(typeId(), obj) + {} - QString algorithm() const - { - QString algo = content(AlgorithmKeyL); - if (!SupportedAlgorithms.contains(algo)) { - qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo - << "is not supported"; - } - return algo; - } + QString algorithm() const; QByteArray ciphertext() const { return content(CiphertextKeyL).toLatin1(); diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index aa05a96e..9eb48844 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptionevent.h" +#include "logging.h" #include "e2ee.h" diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 3d66ab55..305dd454 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -9,15 +9,20 @@ using namespace Quotient; +void Quotient::logFactorySetup(event_mtype_t eventTypeId) +{ + qCDebug(EVENTS) << "Adding factory method for" << eventTypeId; +} + event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) { const auto id = get().eventTypes.size(); get().eventTypes.push_back(matrixTypeId); if (strncmp(matrixTypeId, "", 1) == 0) - qDebug(EVENTS) << "Initialized unknown event type with id" << id; + qCDebug(EVENTS) << "Initialized unknown event type with id" << id; else - qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" - << id; + qCDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" + << id; return id; } diff --git a/lib/events/event.h b/lib/events/event.h index 2ed5de5d..e9963eae 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -4,7 +4,6 @@ #pragma once #include "converters.h" -#include "logging.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === @@ -162,6 +161,8 @@ private: } }; +void logFactorySetup(event_mtype_t eventTypeId); + /** Add a type to its default factory * Adds a standard factory method (via makeEvent<>) for a given * type to EventT::factory_t factory class so that it can be @@ -174,7 +175,7 @@ private: template inline auto setupFactory() { - qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); + logFactorySetup(EventT::matrixTypeId()); return EventT::factory_t::addMethod([](const QJsonObject& json, const QString& jsonMatrixType) { return EventT::matrixTypeId() == jsonMatrixType ? makeEvent(json) diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp index b53fffd6..bb62f0e0 100644 --- a/lib/events/reactionevent.cpp +++ b/lib/events/reactionevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "reactionevent.h" +#include "logging.h" using namespace Quotient; diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index ff93041c..e119696f 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "roomcreateevent.h" +#include "logging.h" using namespace Quotient; diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 332be3f7..dbcb35bd 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "roomkeyevent.h" +#include "logging.h" using namespace Quotient; diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 396e77eb..f9fabdea 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -4,6 +4,7 @@ #include "syncdata.h" #include "events/eventloader.h" +#include "logging.h" #include #include -- 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/events/event.h | 3 +++ lib/room.cpp | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index e9963eae..52e3d025 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -210,7 +210,10 @@ public: Type type() const { return _type; } QString matrixType() const; + [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() " + "or by other means")]] QByteArray originalJson() const; + [[deprecated("Use fullJson() instead")]] // QJsonObject originalJsonObject() const { return fullJson(); } const QJsonObject& fullJson() const { return _json; } 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 4ba795556721a88d2ac258d9095a46de21d77011 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 26 Nov 2021 16:07:25 +0100 Subject: Revert "Drop #include "logging.h" from event.h" Doesn't really help build times, instead breaking the build on older Qt. --- lib/events/callanswerevent.cpp | 1 - lib/events/callhangupevent.cpp | 1 - lib/events/callinviteevent.cpp | 1 - lib/events/encryptedevent.cpp | 11 +++-------- lib/events/encryptedevent.h | 14 ++++++++++---- lib/events/encryptionevent.cpp | 1 - lib/events/event.cpp | 11 +++-------- lib/events/event.h | 5 ++--- lib/events/reactionevent.cpp | 1 - lib/events/roomcreateevent.cpp | 1 - lib/events/roomkeyevent.cpp | 1 - lib/syncdata.cpp | 1 - 12 files changed, 18 insertions(+), 31 deletions(-) diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index 24144585..be83d9d0 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -3,7 +3,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "callanswerevent.h" -#include "logging.h" /* m.call.answer diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp index 537ace75..43bc4db0 100644 --- a/lib/events/callhangupevent.cpp +++ b/lib/events/callhangupevent.cpp @@ -6,7 +6,6 @@ */ #include "callhangupevent.h" -#include "logging.h" /* m.call.hangup diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index ce4fb101..5ea54662 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -3,7 +3,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "callinviteevent.h" -#include "logging.h" /* m.call.invite diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index ebf733ac..0290f973 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" -#include "logging.h" using namespace Quotient; @@ -26,12 +25,8 @@ EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, }) {} -QString EncryptedEvent::algorithm() const +EncryptedEvent::EncryptedEvent(const QJsonObject& obj) + : RoomEvent(typeId(), obj) { - const auto algo = content(AlgorithmKeyL); - if (!SupportedAlgorithms.contains(algo)) { - qCWarning(MAIN) << "The EncryptedEvent's algorithm" << algo - << "is not supported"; - } - return algo; + qCDebug(E2EE) << "Encrypted event from" << senderId(); } diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index d2b71785..598829cd 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -37,11 +37,17 @@ public: /* In case with Megolm, device_id and session_id are required */ explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId); - explicit EncryptedEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) - {} + explicit EncryptedEvent(const QJsonObject& obj); - QString algorithm() const; + QString algorithm() const + { + QString algo = content(AlgorithmKeyL); + if (!SupportedAlgorithms.contains(algo)) { + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + } + return algo; + } QByteArray ciphertext() const { return content(CiphertextKeyL).toLatin1(); diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 9eb48844..aa05a96e 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -3,7 +3,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptionevent.h" -#include "logging.h" #include "e2ee.h" diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 305dd454..3d66ab55 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -9,20 +9,15 @@ using namespace Quotient; -void Quotient::logFactorySetup(event_mtype_t eventTypeId) -{ - qCDebug(EVENTS) << "Adding factory method for" << eventTypeId; -} - event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) { const auto id = get().eventTypes.size(); get().eventTypes.push_back(matrixTypeId); if (strncmp(matrixTypeId, "", 1) == 0) - qCDebug(EVENTS) << "Initialized unknown event type with id" << id; + qDebug(EVENTS) << "Initialized unknown event type with id" << id; else - qCDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" - << id; + qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" + << id; return id; } diff --git a/lib/events/event.h b/lib/events/event.h index 52e3d025..89efb7f8 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -4,6 +4,7 @@ #pragma once #include "converters.h" +#include "logging.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === @@ -161,8 +162,6 @@ private: } }; -void logFactorySetup(event_mtype_t eventTypeId); - /** Add a type to its default factory * Adds a standard factory method (via makeEvent<>) for a given * type to EventT::factory_t factory class so that it can be @@ -175,7 +174,7 @@ void logFactorySetup(event_mtype_t eventTypeId); template inline auto setupFactory() { - logFactorySetup(EventT::matrixTypeId()); + qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); return EventT::factory_t::addMethod([](const QJsonObject& json, const QString& jsonMatrixType) { return EventT::matrixTypeId() == jsonMatrixType ? makeEvent(json) diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp index bb62f0e0..b53fffd6 100644 --- a/lib/events/reactionevent.cpp +++ b/lib/events/reactionevent.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "reactionevent.h" -#include "logging.h" using namespace Quotient; diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index e119696f..ff93041c 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "roomcreateevent.h" -#include "logging.h" using namespace Quotient; diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index dbcb35bd..332be3f7 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "roomkeyevent.h" -#include "logging.h" using namespace Quotient; diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index f9fabdea..396e77eb 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -4,7 +4,6 @@ #include "syncdata.h" #include "events/eventloader.h" -#include "logging.h" #include #include -- 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 +++++++++++++++++------- lib/room.h | 7 ++++--- 2 files changed, 21 insertions(+), 10 deletions(-) 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) diff --git a/lib/room.h b/lib/room.h index 706fb17f..65217290 100644 --- a/lib/room.h +++ b/lib/room.h @@ -212,9 +212,10 @@ public: //! \sa encryptionChanged, upgraded, accountDataChanged Other = 0x8000, //! This is intended to test a Change/Changes value for non-emptiness; - //! testFlag(Change::Any) or adding & Change::Any has - //! the same meaning as !testFlag(Change::None) or adding - //! != Change::None. + //! adding & Change::Any has the same meaning as + //! !testFlag(Change::None) or adding != Change::None + //! \note testFlag(Change::Any) tests that _all_ bits are on and + //! will always return false. Any = 0xFFFF }; QUO_DECLARE_FLAGS(Changes, Change) -- 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(-) 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 ++++++++++++++++---- lib/room.h | 9 ++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) 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) diff --git a/lib/room.h b/lib/room.h index 452ea306..51538b8f 100644 --- a/lib/room.h +++ b/lib/room.h @@ -512,7 +512,14 @@ public: /*! This method returns all known state events that have occured in * the room, as a mapping from the event type and state key to value. */ - Q_INVOKABLE const QHash stateEvents() const; + const QHash& currentState() const; + + /// Get all state events in the room of a certain type. + /*! This method returns all known state events that have occured in + * the room of the given type. + */ + Q_INVOKABLE const QVector + stateEventsOfType(const QString& evtType) const; /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of -- cgit v1.2.3 From c476df5a306383c164b974234c1574f76abb98b8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 27 Nov 2021 18:43:57 +0100 Subject: Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/change-request.md | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/change-request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..637cb4b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** + + +**To Reproduce** +Steps to reproduce the behaviour, and the description of the actual result: +1. +2. +3. + +**Expected behavior** + + +**Is it environment-specific?** + + - OS: [e.g. Windows 11] + - Version of the library [e.g. 0.6.10] + - Linkage: static, dynamic, any + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/change-request.md b/.github/ISSUE_TEMPLATE/change-request.md new file mode 100644 index 00000000..09deeaa6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/change-request.md @@ -0,0 +1,20 @@ +--- +name: Change request +about: Suggest an idea or improvement for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your request related to a problem?** + + +**Describe the suggested change/improvement you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + -- cgit v1.2.3 From 1903a8e26893a3f332ba39643aa8da5fdaf1536b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 27 Nov 2021 18:45:03 +0100 Subject: Delete ISSUE_TEMPLATE.md New issue templates reside under .github/ --- ISSUE_TEMPLATE.md | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 1e3f172b..00000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,48 +0,0 @@ - - -### Description - -Describe here the problem that you are experiencing, or the feature -you are requesting. - -### Steps to reproduce - -- For bugs, list the steps -- that reproduce the bug -- using hyphens as bullet points - -Describe how what happens differs from what you expected. - -libQuotient-based clients either have a log file or dump log -to the standard output. If you can identify any log snippets relevant -to your issue, please include those here (please be careful to remove -any personal or private data): - -### Version information - - - -- **The client application**: - -- **libQuotient version if you know it**: - -- **Qt version**: - -- **Install method**: - -- **Platform**: - -- cgit v1.2.3 From 0a46049ef26270933ecf6fea7395b03e6aee783e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 27 Nov 2021 10:17:51 +0100 Subject: Add SonarCloud analysis to CI --- .github/workflows/ci.yml | 3 +- .github/workflows/sonar.yml | 113 ++++++++++++++++++++++++++++++++++++++++++++ sonar-project.properties | 11 +++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sonar.yml create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 714473b0..47e55421 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,7 @@ jobs: steps: - uses: actions/checkout@v2 with: + fetch-depth: 0 submodules: ${{ matrix.e2ee != '' }} - name: Cache Qt @@ -126,7 +127,7 @@ jobs: -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV - + - name: Initialize CodeQL tools if: env.CODEQL_ANALYSIS uses: github/codeql-action/init@v1 diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 00000000..76db59c9 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,113 @@ +name: Sonar + +on: + push: + pull_request: + types: [opened, reopened] + +defaults: + run: + shell: bash + +jobs: + SonarCloud: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + qt-version: [ '5.12.10' ] + e2ee: [ '', 'e2ee' ] + update-api: [ '', 'update-api' ] + + env: + SONAR_SCANNER_VERSION: 4.6.2.2472 + SONAR_SERVER_URL: "https://sonarcloud.io" + BUILD_WRAPPER_OUT_DIR: build/sonar + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: ${{ matrix.e2ee != '' }} + + - name: Cache Qt + id: cache-qt + uses: actions/cache@v2 + with: + path: ${{ runner.workspace }}/Qt + key: ${{ runner.os }}-Qt${{ matrix.qt-version }}-cache + + - name: Install Qt + uses: jurplel/install-qt-action@v2.11.1 + with: + version: ${{ matrix.qt-version }} +# arch: ${{ matrix.qt-arch }} # Only Windows needs that + cached: ${{ steps.cache-qt.outputs.cache-hit }} + + - name: Install Ninja + uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Setup build environment + run: | + echo "CC=gcc-10" >>$GITHUB_ENV + echo "CXX=g++-10" >>$GITHUB_ENV + mkdir -p $HOME/.sonar + echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ + -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV + cmake -E make_directory ${{ runner.workspace }}/build + + - name: Build and install olm + if: matrix.e2ee + run: | + cd .. + git clone https://gitlab.matrix.org/matrix-org/olm.git + cmake -S olm -B olm/build $CMAKE_ARGS + cmake --build olm/build --target install + + - name: Pull CS API and build GTAD + if: matrix.update-api + run: | + cd .. + git clone https://github.com/quotient-im/matrix-doc.git + git clone --recursive https://github.com/KitsuneRal/gtad.git + cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF + cmake --build gtad + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ + -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ + >>$GITHUB_ENV + + - name: Download and set up Sonar Cloud tools + run: | + pushd $HOME/.sonar + curl -sSLo build-wrapper.zip $SONAR_SERVER_URL/static/cpp/build-wrapper-linux-x86.zip + unzip -o build-wrapper.zip + echo "BUILD_WRAPPER=$HOME/.sonar/build-wrapper-linux-x86/build-wrapper-linux* --out-dir $BUILD_WRAPPER_OUT_DIR" >>$GITHUB_ENV + curl -sSLo sonar-scanner.zip \ + https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip + unzip -o sonar-scanner.zip + popd + + - name: Configure libQuotient + run: | + if [[ '${{ runner.os }}' == 'Windows' ]]; then + BIN_DIR=. + else + BIN_DIR=bin + fi + echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV + cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} + + - name: Regenerate API code + if: matrix.update-api + run: cmake --build build --target update-api + + - name: Build libQuotient + run: | + $BUILD_WRAPPER cmake --build build --target all + + - name: Run sonar-scanner + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + $HOME/.sonar/sonar-scanner*/bin/sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..96e98985 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=quotient-im_libQuotient +sonar.organization=quotient-im + +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=libQuotient +sonar.projectVersion=0.7 + +sonar.sources=lib + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 -- 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/events/accountdataevents.h | 2 +- lib/events/callanswerevent.h | 4 ++-- lib/events/callcandidatesevent.h | 6 +++--- lib/events/callinviteevent.h | 4 ++-- lib/events/encryptedevent.h | 12 ++++++------ lib/events/event.h | 15 +++++++++++---- lib/events/reactionevent.h | 2 +- lib/events/redactionevent.h | 2 +- lib/events/roomcreateevent.cpp | 8 ++++---- lib/events/roomevent.h | 4 ++-- lib/events/roomkeyevent.h | 8 ++++---- lib/events/roommessageevent.cpp | 4 ++-- lib/events/roomtombstoneevent.cpp | 4 ++-- lib/events/stickerevent.cpp | 2 +- lib/events/typingevent.cpp | 2 +- lib/room.cpp | 4 ++-- 16 files changed, 45 insertions(+), 38 deletions(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 8cea0ec8..7715d3b8 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -63,7 +63,7 @@ using TagsMap = QHash; {} \ auto _ContentKey() const \ { \ - return content(#_ContentKey##_ls); \ + return contentPart(#_ContentKey##_ls); \ } \ }; \ REGISTER_EVENT_TYPE(_Name) \ diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 6132cb44..4c01c941 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -19,11 +19,11 @@ public: int lifetime() const { - return content("lifetime"_ls); + return contentPart("lifetime"_ls); } // FIXME: Omittable<>? QString sdp() const { - return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString(); + return contentPart("answer"_ls).value("sdp"_ls).toString(); } }; diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index c2ccac3b..74c38f2c 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -25,17 +25,17 @@ public: QJsonArray candidates() const { - return content("candidates"_ls); + return contentPart("candidates"_ls); } QString callId() const { - return content("call_id"); + return contentPart("call_id"); } int version() const { - return content("version"); + return contentPart("version"); } }; diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index d3454c4f..80b7d651 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -18,11 +18,11 @@ public: int lifetime() const { - return content("lifetime"_ls); + return contentPart("lifetime"_ls); } // FIXME: Omittable<>? QString sdp() const { - return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString(); + return contentPart("offer"_ls).value("sdp"_ls).toString(); } }; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 598829cd..de89a7c6 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -41,7 +41,7 @@ public: QString algorithm() const { - QString algo = content(AlgorithmKeyL); + QString algo = contentPart(AlgorithmKeyL); if (!SupportedAlgorithms.contains(algo)) { qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo << "is not supported"; @@ -50,17 +50,17 @@ public: } QByteArray ciphertext() const { - return content(CiphertextKeyL).toLatin1(); + return contentPart(CiphertextKeyL).toLatin1(); } QJsonObject ciphertext(const QString& identityKey) const { - return content(CiphertextKeyL).value(identityKey).toObject(); + return contentPart(CiphertextKeyL).value(identityKey).toObject(); } - QString senderKey() const { return content(SenderKeyKeyL); } + QString senderKey() const { return contentPart(SenderKeyKeyL); } /* device_id and session_id are required with Megolm */ - QString deviceId() const { return content(DeviceIdKeyL); } - QString sessionId() const { return content(SessionIdKeyL); } + QString deviceId() const { return contentPart(DeviceIdKeyL); } + QString sessionId() const { return contentPart(SessionIdKeyL); } }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/events/event.h b/lib/events/event.h index 89efb7f8..024e45ef 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -224,18 +224,25 @@ public: const QJsonObject contentJson() const; const QJsonObject unsignedJson() const; - template - T content(const QString& key) const + template + const T contentPart(const QString& key) const { return fromJson(contentJson()[key]); } - template - T content(QLatin1String key) const + template + const T contentPart(QLatin1String key) const { return fromJson(contentJson()[key]); } + template + [[deprecated("Use contentPart() to get a part of the event content")]] // + T content(const QString& key) const + { + return contentPart(key); + } + friend QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 777905f2..5a2b98c4 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -47,7 +47,7 @@ public: explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} EventRelation relation() const { - return content(QStringLiteral("m.relates_to")); + return contentPart("m.relates_to"_ls); } }; REGISTER_EVENT_TYPE(ReactionEvent) diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index ed560331..be20bf52 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -17,7 +17,7 @@ public: { return fullJson()["redacts"_ls].toString(); } - QString reason() const { return contentJson()["reason"_ls].toString(); } + QString reason() const { return contentPart("reason"_ls); } }; REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index ff93041c..bb6de648 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -23,17 +23,17 @@ struct Quotient::JsonConverter { bool RoomCreateEvent::isFederated() const { - return fromJson(contentJson()["m.federate"_ls]); + return contentPart("m.federate"_ls); } QString RoomCreateEvent::version() const { - return fromJson(contentJson()["room_version"_ls]); + return contentPart("room_version"_ls); } RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const { - const auto predJson = contentJson()["predecessor"_ls].toObject(); + const auto predJson = contentPart("predecessor"_ls); return { fromJson(predJson[RoomIdKeyL]), fromJson(predJson[EventIdKeyL]) }; } @@ -45,5 +45,5 @@ bool RoomCreateEvent::isUpgrade() const RoomType RoomCreateEvent::roomType() const { - return fromJson(contentJson()["type"_ls]); + return contentPart("type"_ls); } diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index a6cd84ca..7f13f6f2 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -78,8 +78,8 @@ public: ~CallEventBase() override = default; bool isCallEvent() const override { return true; } - QString callId() const { return content("call_id"_ls); } - int version() const { return content("version"_ls); } + QString callId() const { return contentPart("call_id"_ls); } + int version() const { return contentPart("version"_ls); } }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 14e80324..d021fbec 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -13,10 +13,10 @@ public: explicit RoomKeyEvent(const QJsonObject& obj); - QString algorithm() const { return content("algorithm"_ls); } - QString roomId() const { return content(RoomIdKeyL); } - QString sessionId() const { return content("session_id"_ls); } - QString sessionKey() const { return content("session_key"_ls); } + QString algorithm() const { return contentPart("algorithm"_ls); } + QString roomId() const { return contentPart(RoomIdKeyL); } + QString sessionId() const { return contentPart("session_id"_ls); } + QString sessionKey() const { return contentPart("session_key"_ls); } }; REGISTER_EVENT_TYPE(RoomKeyEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 9b46594e..2b7b4166 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -204,12 +204,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentJson()[MsgTypeKeyL].toString(); + return contentPart(MsgTypeKeyL); } QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKeyL].toString(); + return contentPart(BodyKeyL); } QMimeType RoomMessageEvent::mimeType() const diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp index 080d269c..2c3492d6 100644 --- a/lib/events/roomtombstoneevent.cpp +++ b/lib/events/roomtombstoneevent.cpp @@ -7,10 +7,10 @@ using namespace Quotient; QString RoomTombstoneEvent::serverMessage() const { - return fromJson(contentJson()["body"_ls]); + return contentPart("body"_ls); } QString RoomTombstoneEvent::successorRoomId() const { - return fromJson(contentJson()["replacement_room"_ls]); + return contentPart("replacement_room"_ls); } diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp index ea4dff3f..628fd154 100644 --- a/lib/events/stickerevent.cpp +++ b/lib/events/stickerevent.cpp @@ -12,7 +12,7 @@ StickerEvent::StickerEvent(const QJsonObject &obj) QString StickerEvent::body() const { - return content("body"_ls); + return contentPart("body"_ls); } const EventContent::ImageContent &StickerEvent::image() const diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp index e97e978f..7e5d7ee6 100644 --- a/lib/events/typingevent.cpp +++ b/lib/events/typingevent.cpp @@ -7,5 +7,5 @@ using namespace Quotient; QStringList TypingEvent::users() const { - return fromJson(contentJson()["user_ids"_ls]); + return contentPart("user_ids"_ls); } 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(-) 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 ce4e878040946b963b30c4768847e2be24dd77fa Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 27 Nov 2021 22:53:02 +0100 Subject: basicEventJson(): dismiss with the template Given that QJsonObject only accepts QStrings in the list constructor, the template is useless cruft. --- lib/events/event.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 024e45ef..733fadd8 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -48,11 +48,10 @@ static const auto PrevContentKeyL = "prev_content"_ls; static const auto StateKeyKeyL = "state_key"_ls; /// Make a minimal correct Matrix event JSON -template -inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) +inline QJsonObject basicEventJson(const QString& matrixType, + const QJsonObject& content) { - return { { TypeKey, std::forward(matrixType) }, - { ContentKey, content } }; + return { { TypeKey, matrixType }, { ContentKey, content } }; } // === Event types and event types registry === -- cgit v1.2.3 From ab91944cc71a72699d0168b7c472326e59319477 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 11:19:08 +0100 Subject: Event::unsignedPart() Similar to contentPart() - apparently there are enough places across the code that would benefit from it. --- lib/events/event.h | 21 +++++++++++---------- lib/events/roomevent.cpp | 10 +++++----- lib/events/stateevent.cpp | 10 +++++----- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 733fadd8..e45fecca 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -221,18 +221,11 @@ public: // different types, we're implementing it per-event type. const QJsonObject contentJson() const; - const QJsonObject unsignedJson() const; - template - const T contentPart(const QString& key) const + template + const T contentPart(KeyT&& key) const { - return fromJson(contentJson()[key]); - } - - template - const T contentPart(QLatin1String key) const - { - return fromJson(contentJson()[key]); + return fromJson(contentJson()[std::forward(key)]); } template @@ -242,6 +235,14 @@ public: return contentPart(key); } + const QJsonObject unsignedJson() const; + + template + const T unsignedPart(KeyT&& key) const + { + return fromJson(unsignedJson()[std::forward(key)]); + } + friend QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 4fec9d2b..fb921af6 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -19,7 +19,7 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { - if (const auto redaction = unsignedJson()[RedactedCauseKeyL]; + if (const auto redaction = unsignedPart(RedactedCauseKeyL); redaction.isObject()) _redactedBecause = makeEvent(redaction.toObject()); } @@ -45,14 +45,14 @@ QString RoomEvent::senderId() const bool RoomEvent::isReplaced() const { - return unsignedJson()["m.relations"_ls].toObject().contains("m.replace"); + return unsignedPart("m.relations"_ls).contains("m.replace"); } QString RoomEvent::replacedBy() const { // clang-format off - return unsignedJson()["m.relations"_ls].toObject() - .value("m.replace").toObject() + return unsignedPart("m.relations"_ls) + .value("m.replace"_ls).toObject() .value(EventIdKeyL).toString(); // clang-format on } @@ -64,7 +64,7 @@ QString RoomEvent::redactionReason() const QString RoomEvent::transactionId() const { - return unsignedJson()["transaction_id"_ls].toString(); + return unsignedPart("transaction_id"_ls); } QString RoomEvent::stateKey() const diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 42fc9054..efe011a0 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -28,22 +28,22 @@ StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, bool StateEventBase::repeatsState() const { - const auto prevContentJson = unsignedJson().value(PrevContentKeyL); + const auto prevContentJson = unsignedPart(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } QString StateEventBase::replacedState() const { - return unsignedJson().value("replaces_state"_ls).toString(); + return unsignedPart("replaces_state"_ls); } void StateEventBase::dumpTo(QDebug dbg) const { if (!stateKey().isEmpty()) dbg << '<' << stateKey() << "> "; - if (unsignedJson().contains(PrevContentKeyL)) - dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) - .toJson(QJsonDocument::Compact) + if (const auto prevContentJson = unsignedPart(PrevContentKeyL); + !prevContentJson.isEmpty()) + dbg << QJsonDocument(prevContentJson).toJson(QJsonDocument::Compact) << " -> "; RoomEvent::dumpTo(dbg); } -- cgit v1.2.3 From e12a7aac030aaf53122da4b1bd55f3a97325f624 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 15:44:41 +0100 Subject: Fix CI breakage caused by the previous commit --- lib/converters.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/converters.h b/lib/converters.h index 8ec0fa81..f6d56527 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -62,6 +62,8 @@ inline T fromJson(const QJsonValue& jv) return JsonConverter::load(jv); } +inline QJsonValue fromJson(const QJsonValue& jv) { return jv; } + template inline T fromJson(const QJsonDocument& jd) { -- cgit v1.2.3 From 080af15c8aa7cc51f97c924ba4678083b7f8da8f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 16:04:42 +0100 Subject: One more small thing to actually fix CI breakage It's might look weird; but without making fromJson() a specialisation it becomes an overload next to an implicit specialisation of the template function defined just above, and then loses to that specialisation because it (also) has the perfect match. (would be great if the compiler shaded the implicit specialisation in such cases - alas it's not how the standard works.) --- lib/converters.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/converters.h b/lib/converters.h index f6d56527..84fab798 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -62,6 +62,7 @@ inline T fromJson(const QJsonValue& jv) return JsonConverter::load(jv); } +template<> inline QJsonValue fromJson(const QJsonValue& jv) { return jv; } template -- cgit v1.2.3 From ca1ba482b50c41425bd0a540c7bb68406d10e552 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 16:42:41 +0100 Subject: Don't std::move when the callee doesn't support it In both fixed cases the callee accepts a const reference, which makes std::move() useless. Static analyzers apparently missed them because the cases are inside a macro. --- lib/events/accountdataevents.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 7715d3b8..e5101d20 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -55,11 +55,11 @@ using TagsMap = QHash; public: \ using content_type = _ContentType; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \ - explicit _Name(_ContentType content) \ + explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \ + explicit _Name(const content_type& content) \ : Event(typeId(), matrixTypeId(), \ - QJsonObject { { QStringLiteral(#_ContentKey), \ - toJson(std::move(content)) } }) \ + QJsonObject { \ + { QStringLiteral(#_ContentKey), toJson(content) } }) \ {} \ auto _ContentKey() const \ { \ -- cgit v1.2.3 From 8a769cddcd1a063dd9400518ff65c0b1f1aec1b4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 16:53:51 +0100 Subject: Comment on const return types in event.h Proper linters recognise that the returned types are not primitive, while people might still be confused a bit. --- lib/events/event.cpp | 3 --- lib/events/event.h | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 3d66ab55..96be717c 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -46,14 +46,11 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); } -// On const below: this is to catch accidental attempts to change event JSON -// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::contentJson() const { return fullJson()[ContentKeyL].toObject(); } -// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); diff --git a/lib/events/event.h b/lib/events/event.h index e45fecca..998a386c 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -220,6 +220,9 @@ public: // a "content" object; but since its structure is different for // different types, we're implementing it per-event type. + // NB: const return types below are meant to catch accidental attempts + // to change event JSON (e.g., consider contentJson()["inexistentKey"]). + const QJsonObject contentJson() const; template -- cgit v1.2.3 From efd07e0f728d08ff76975c7b053104ab05c48799 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 16:54:52 +0100 Subject: CMakeLists: drop obsolete -W from the warnings list Turns out it's been deprecated by -Wextra since before Quotient existed. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eaf662cc..aa3b9c98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ if (MSVC) /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() - foreach (FLAG Wall W Wpedantic Wextra Werror=return-type Wno-unused-parameter + foreach (FLAG Wall Wpedantic Wextra Werror=return-type Wno-unused-parameter Wno-gnu-zero-variadic-macro-arguments fvisibility-inlines-hidden) CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) if ( COMPILER_${FLAG}_SUPPORTED AND -- cgit v1.2.3 From 121c2a08b25d79bcfd09c050d72c6ea7cc7720f9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 28 Nov 2021 17:21:01 +0100 Subject: Simplify converters.* There was a lot of excess redirection in fromJson() and toJson() with most of JsonConverter<> specialisations being unnecessary boilerplate. These have been replaced by overloads for toJson() and explicit specialisations for fromJson() wherever possible without breaking the conversion logic. --- lib/converters.cpp | 11 +++-- lib/converters.h | 126 +++++++++++++++++++++++++---------------------------- 2 files changed, 64 insertions(+), 73 deletions(-) diff --git a/lib/converters.cpp b/lib/converters.cpp index 4338e8ed..444ca4f6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -5,24 +5,23 @@ #include -using namespace Quotient; - -QJsonValue JsonConverter::dump(const QVariant& v) +QJsonValue Quotient::JsonConverter::dump(const QVariant& v) { return QJsonValue::fromVariant(v); } -QVariant JsonConverter::load(const QJsonValue& jv) +QVariant Quotient::JsonConverter::load(const QJsonValue& jv) { return jv.toVariant(); } -QJsonObject JsonConverter::dump(const QVariantHash& vh) +QJsonObject Quotient::toJson(const QVariantHash& vh) { return QJsonObject::fromVariantHash(vh); } -QVariantHash JsonConverter::load(const QJsonValue& jv) +template<> +QVariantHash Quotient::fromJson(const QJsonValue& jv) { return jv.toObject().toVariantHash(); } diff --git a/lib/converters.h b/lib/converters.h index 84fab798..90349019 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -24,6 +24,21 @@ struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } }; +//! \brief The switchboard for extra conversion algorithms behind from/toJson +//! +//! This template is mainly intended for partial conversion specialisations +//! since from/toJson are functions and cannot be partially specialised; +//! another case for JsonConverter is to insulate types that can be constructed +//! from basic types - namely, QVariant and QUrl can be directly constructed +//! from QString and having an overload or specialisation for those leads to +//! ambiguity between these and QJsonValue. For trivial (converting +//! QJsonObject/QJsonValue) and most simple cases such as primitive types or +//! QString this class is not needed. +//! +//! Do NOT call the functions of this class directly unless you know what you're +//! doing; and do not try to specialise basic things unless you're really sure +//! that they are not supported and it's not feasible to support those by means +//! of overloading toJson() and specialising fromJson(). template struct JsonConverter { static QJsonObject dump(const T& pod) @@ -42,13 +57,17 @@ struct JsonConverter { static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } }; -template +template >> inline auto toJson(const T& pod) +// -> can return anything from which QJsonValue or, in some cases, QJsonDocument +// is constructible { return JsonConverter::dump(pod); } inline auto toJson(const QJsonObject& jo) { return jo; } +inline auto toJson(const QJsonValue& jv) { return jv; } template inline void fillJson(QJsonObject& json, const T& data) @@ -98,80 +117,64 @@ inline void fillFromJson(const QJsonValue& jv, T& pod) // JsonConverter<> specialisations -template -struct TrivialJsonDumper { - // Works for: QJsonValue (and all things it can consume), - // QJsonObject, QJsonArray - static auto dump(const T& val) { return val; } -}; +template<> +inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); } template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return jv.toBool(); } -}; +inline int fromJson(const QJsonValue& jv) { return jv.toInt(); } template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return jv.toInt(); } -}; +inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); } template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return jv.toDouble(); } -}; +inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); } template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } -}; +inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } -}; +inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return jv.toString(); } -}; +inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } template <> -struct JsonConverter { - static auto dump(const QDateTime& val) - { - return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); - } - static auto load(const QJsonValue& jv) - { - return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); - } -}; +inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); } +inline QJsonValue toJson(const QDateTime& val) +{ + return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); +} template <> -struct JsonConverter { - static auto dump(const QDate& val) { - return toJson( +inline QDateTime fromJson(const QJsonValue& jv) +{ + return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); +} + +inline QJsonValue toJson(const QDate& val) { + return toJson( #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QDateTime(val) + QDateTime(val) #else - val.startOfDay() + val.startOfDay() #endif - ); - } - static auto load(const QJsonValue& jv) - { - return fromJson(jv).date(); - } -}; + ); +} +template <> +inline QDate fromJson(const QJsonValue& jv) +{ + return fromJson(jv).date(); +} + +// Insulate QVariant and QUrl conversions into JsonConverter so that they don't +// interfere with toJson(const QJsonValue&) over QString, since both types are +// constructible from QString (even if QUrl requires explicit construction). template <> struct JsonConverter { static auto load(const QJsonValue& jv) { - // QT_NO_URL_CAST_FROM_STRING makes this a bit more verbose - QUrl url; - url.setUrl(jv.toString()); - return url; + return QUrl(jv.toString()); } static auto dump(const QUrl& url) { @@ -179,11 +182,6 @@ struct JsonConverter { } }; -template <> -struct JsonConverter : public TrivialJsonDumper { - static auto load(const QJsonValue& jv) { return jv.toArray(); } -}; - template <> struct JsonConverter { static QJsonValue dump(const QVariant& v); @@ -206,15 +204,11 @@ struct JsonConverter> { template struct JsonArrayConverter { - static void dumpTo(QJsonArray& ar, const VectorT& vals) - { - for (const auto& v : vals) - ar.push_back(toJson(v)); - } static auto dump(const VectorT& vals) { QJsonArray ja; - dumpTo(ja, vals); + for (const auto& v : vals) + ja.push_back(toJson(v)); return ja; } static auto load(const QJsonArray& ja) @@ -254,7 +248,7 @@ struct JsonObjectConverter> { static void dumpTo(QJsonObject& json, const QSet& s) { for (const auto& e : s) - json.insert(toJson(e), QJsonObject {}); + json.insert(e, QJsonObject {}); } static void fillFrom(const QJsonObject& json, QSet& s) { @@ -287,11 +281,9 @@ template struct JsonObjectConverter> : public HashMapFromJson> {}; +QJsonObject toJson(const QVariantHash& vh); template <> -struct JsonConverter { - static QJsonObject dump(const QVariantHash& vh); - static QVariantHash load(const QJsonValue& jv); -}; +QVariantHash fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject -- 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(+) 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 3849a2466208a741165385f20150bddad59d2b8c Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 22 Jan 2021 23:36:47 +0100 Subject: Start implementing Qt olm binding --- CMakeLists.txt | 3 +- lib/olm/qolmaccount.cpp | 173 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/olm/qolmaccount.h | 89 +++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 lib/olm/qolmaccount.cpp create mode 100644 lib/olm/qolmaccount.h diff --git a/CMakeLists.txt b/CMakeLists.txt index aa3b9c98..b4d4ef8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,6 +171,7 @@ list(APPEND lib_SRCS lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp + lib/olm/qolmaccount.cpp ) # Configure API files generation @@ -317,7 +318,7 @@ target_include_directories(${PROJECT_NAME} PUBLIC $ ) if (${PROJECT_NAME}_ENABLE_E2EE) - target_link_libraries(${PROJECT_NAME} QtOlm) + target_link_libraries(${PROJECT_NAME} Olm::Olm QtOlm) set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in endif() target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp new file mode 100644 index 00000000..673f613b --- /dev/null +++ b/lib/olm/qolmaccount.cpp @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "qolmaccount.h" +#include +#include +#include +#include + +QMap OneTimeKeys::curve25519() const +{ + return keys[QStringLiteral("curve25519")]; +} + +std::optional> OneTimeKeys::get(QString keyType) const +{ + if (!keys.contains(keyType)) { + return std::nullopt; + } + return keys[keyType]; +} + +// Convert PicklingMode to key +QByteArray toKey(PicklingMode mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) +{ + return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; +} + +// Conver olm error to enum +QOlmAccount::OlmAccountError lastError(OlmAccount *account) { + const std::string error_raw = olm_account_last_error(account); + + if (error_raw.compare("BAD_ACCOUNT_KEY")) { + return QOlmAccount::OlmAccountError::BadAccountKey; + } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { + return QOlmAccount::OlmAccountError::BadMessageKeyId; + } else if (error_raw.compare("INVALID_BASE64")) { + return QOlmAccount::OlmAccountError::InvalidBase64; + } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { + return QOlmAccount::OlmAccountError::NotEnoughRandom; + } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + return QOlmAccount::OlmAccountError::OutputBufferTooSmall; + } else { + return QOlmAccount::OlmAccountError::Unknown; + } +} + +QByteArray getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + std::generate(buffer.begin(), buffer.end(), std::rand); + return buffer; +} + +QOlmAccount::QOlmAccount(OlmAccount *account) + : m_account(account) +{} + +std::optional QOlmAccount::create() +{ + auto account = olm_account(new uint8_t[olm_account_size()]); + size_t randomSize = olm_create_account_random_length(account); + QByteArray randomData = getRandom(randomSize); + const auto error = olm_create_account(account, randomData.data(), randomSize); + if (error == olm_error()) { + return std::nullopt; + } + return std::make_optional(account); +} + +std::variant QOlmAccount::unpickle(QByteArray pickled, PicklingMode mode) +{ + auto account = olm_account(new uint8_t[olm_account_size()]); + const QByteArray key = toKey(mode); + const auto error = olm_unpickle_account(account, key.data(), key.length(), pickled.data(), pickled.size()); + if (error == olm_error()) { + return lastError(account); + } + return QOlmAccount(account); +} + +std::variant QOlmAccount::pickle(PicklingMode mode) +{ + const QByteArray key = toKey(mode); + const size_t pickleLength = olm_pickle_account_length(m_account); + QByteArray pickleBuffer(pickleLength, '0'); + const auto error = olm_pickle_account(m_account, key.data(), + key.length(), pickleBuffer.data(), pickleLength); + if (error == olm_error()) { + return lastError(m_account); + } + return pickleBuffer; +} + +std::variant QOlmAccount::identityKeys() +{ + const size_t keyLength = olm_account_identity_keys_length(m_account); + QByteArray keyBuffer(keyLength, '0'); + const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); + if (error == olm_error()) { + return lastError(m_account); + } + const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); + return IdentityKeys { + key.value(QStringLiteral("curve25519")).toString().toUtf8(), + key.value(QStringLiteral("ed25519")).toString().toUtf8() + }; +} + +std::variant QOlmAccount::sign(QString message) const +{ + const size_t signatureLength = olm_account_signature_length(m_account); + QByteArray signatureBuffer(signatureLength, '0'); + const auto error = olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureLength); + + if (error == olm_error()) { + return lastError(m_account); + } + return QString::fromUtf8(signatureBuffer); +} + +size_t QOlmAccount::maxNumberOfOneTimeKeys() const +{ + return olm_account_max_number_of_one_time_keys(m_account); +} + +std::optional QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +{ + const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + QByteArray randomBuffer = getRandom(randomLen); + const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLen); + + if (error == olm_error()) { + return lastError(m_account); + } + return std::nullopt; +} + +std::variant QOlmAccount::oneTimeKeys() const +{ + const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); + + const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); + if (error == olm_error()) { + return lastError(m_account); + } + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); + OneTimeKeys oneTimeKeys; + + for (const QJsonValue &key1 : json.keys()) { + auto oneTimeKeyObject = json[key1.toString()].toObject(); + auto keyMap = QMap(); + for (const QString &key2 : oneTimeKeyObject.keys()) { + keyMap[key2] = oneTimeKeyObject[key2].toString(); + } + oneTimeKeys.keys[key1.toString()] = keyMap; + } + return oneTimeKeys; +} + +#endif diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h new file mode 100644 index 00000000..219d7e48 --- /dev/null +++ b/lib/olm/qolmaccount.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once + +#include +#include +#include +#include +#include +#include "olm/olm.h" + +struct OlmAccount; + +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant; + +template struct overloaded : Ts... { using Ts::operator()...; }; +template overloaded(Ts...) -> overloaded; + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the the one-time keys. +struct OneTimeKeys +{ + QMap> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QMap curve25519() const; + + //! Get a reference to the hashmap corresponding to given key type. + std::optional> get(QString keyType) const; +}; + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); + +//! An olm account manages all cryptographic keys used on a device. +//! \code{.cpp} +//! const auto olmAccount = new QOlmAccount(this); +//! \endcode +class QOlmAccount +{ +public: + enum OlmAccountError { + BadAccountKey, + BadMessageKeyId, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + Unknown, + }; + + //! Creates a new instance of OlmAccount. During the instantiation + //! the Ed25519 fingerprint key pair and the Curve25519 identity key + //! pair are generated. For more information see here. + static std::optional create(); + static std::variant unpickle(QByteArray picked, PicklingMode mode); + + //! Serialises an OlmAccount to encrypted Base64. + std::variant pickle(PicklingMode mode); + std::variant identityKeys(); + + //! Returns the signature of the supplied message. + std::variant sign(QString message) const; + + //! Maximum number of one time keys that this OlmAccount can + //! currently hold. + size_t maxNumberOfOneTimeKeys() const; + + //! Generates the supplied number of one time keys. + std::optional generateOneTimeKeys(size_t numberOfKeys) const; + + //! Gets the OlmAccount's one time keys formatted as JSON. + std::variant oneTimeKeys() const; + + // HACK do not use directly + QOlmAccount(OlmAccount *account); +private: + OlmAccount *m_account; +}; -- cgit v1.2.3 From d9286b1ad5516082bc9b40adaceb9485acf4a553 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 22 Jan 2021 23:38:32 +0100 Subject: Add tests --- autotests/CMakeLists.txt | 1 + autotests/testolmaccount.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++++ autotests/testolmaccount.h | 19 +++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 autotests/testolmaccount.cpp create mode 100644 autotests/testolmaccount.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 282ab036..07c22ad6 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -12,3 +12,4 @@ function(QUOTIENT_ADD_TEST) endfunction() quotient_add_test(NAME callcandidateseventtest) +quotient_add_test(NAME testolmaccount) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp new file mode 100644 index 00000000..549f07ea --- /dev/null +++ b/autotests/testolmaccount.cpp @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "testolmaccount.h" +#include "olm/qolmaccount.h" + +void TestOlmAccount::pickleUnpickedTest() +{ + auto olmAccount = QOlmAccount::create().value(); + auto identityKeys = std::get(olmAccount.identityKeys()); + auto pickled = std::get(olmAccount.pickle(Unencrypted{})); + auto olmAccount2 = std::get(QOlmAccount::unpickle(pickled, Unencrypted{})); + auto identityKeys2 = std::get(olmAccount2.identityKeys()); + QCOMPARE(identityKeys.curve25519, identityKeys2.curve25519); + QCOMPARE(identityKeys.ed25519, identityKeys2.ed25519); +} + +void TestOlmAccount::identityKeysValid() +{ + auto olmAccount = QOlmAccount::create().value(); + const auto identityKeys = std::get(olmAccount.identityKeys()); + const auto curve25519 = identityKeys.curve25519; + const auto ed25519 = identityKeys.ed25519; + // verify encoded keys length + QCOMPARE(curve25519.size(), 43); + QCOMPARE(ed25519.size(), 43); + + // encoded as valid base64? + QVERIFY(QByteArray::fromBase64Encoding(curve25519).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QVERIFY(QByteArray::fromBase64Encoding(ed25519).decodingStatus == QByteArray::Base64DecodingStatus::Ok); +} + +void TestOlmAccount::signatureValid() +{ + const auto olmAccount = QOlmAccount::create().value(); + const auto message = "Hello world!"; + const auto signature = std::get(olmAccount.sign(message)); + QVERIFY(QByteArray::fromBase64Encoding(signature.toUtf8()).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + + //let utility = OlmUtility::new(); + //let identity_keys = olm_account.parsed_identity_keys(); + //let ed25519_key = identity_keys.ed25519(); + //assert!(utility + // .ed25519_verify(&ed25519_key, message, &signature) + // .unwrap()); +} + +void TestOlmAccount::oneTimeKeysValid() +{ + const auto olmAccount = QOlmAccount::create().value(); + const auto maxNumberOfOneTimeKeys = olmAccount.maxNumberOfOneTimeKeys(); + QCOMPARE(100, maxNumberOfOneTimeKeys); + + const auto oneTimeKeysEmpty = std::get(olmAccount.oneTimeKeys()); + QVERIFY(oneTimeKeysEmpty.curve25519().isEmpty()); + + olmAccount.generateOneTimeKeys(20); + const auto oneTimeKeysFilled = std::get(olmAccount.oneTimeKeys()); + QCOMPARE(20, oneTimeKeysFilled.curve25519().count()); +} + +QTEST_MAIN(TestOlmAccount) +#endif diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h new file mode 100644 index 00000000..c3297b5f --- /dev/null +++ b/autotests/testolmaccount.h @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include + +class TestOlmAccount : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void pickleUnpickedTest(); + void identityKeysValid(); + void signatureValid(); + void oneTimeKeysValid(); + //void removeOneTimeKeys(); +}; +#endif -- cgit v1.2.3 From c794d61b161ad48312bf079f1e5f483cfac1dc38 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 22 Jan 2021 23:45:34 +0100 Subject: Add destructor --- lib/olm/qolmaccount.cpp | 5 +++++ lib/olm/qolmaccount.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index 673f613b..a6a07962 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -66,6 +66,11 @@ QOlmAccount::QOlmAccount(OlmAccount *account) : m_account(account) {} +QOlmAccount::~QOlmAccount() +{ + olm_clear_account(m_account); +} + std::optional QOlmAccount::create() { auto account = olm_account(new uint8_t[olm_account_size()]); diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h index 219d7e48..268cd5d5 100644 --- a/lib/olm/qolmaccount.h +++ b/lib/olm/qolmaccount.h @@ -49,6 +49,8 @@ bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); class QOlmAccount { public: + ~QOlmAccount(); + enum OlmAccountError { BadAccountKey, BadMessageKeyId, -- cgit v1.2.3 From e2075a1f33f7987385fc61338ce1756715fdaf6a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 23 Jan 2021 15:57:39 +0100 Subject: Start inboundsession wrapper --- lib/olm/errors.h | 46 ++++++++++++++++++++++++++++++++++++++++++ lib/olm/qolminboundsession.cpp | 30 +++++++++++++++++++++++++++ lib/olm/qolminboundsession.h | 21 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 lib/olm/errors.h create mode 100644 lib/olm/qolminboundsession.cpp create mode 100644 lib/olm/qolminboundsession.h diff --git a/lib/olm/errors.h b/lib/olm/errors.h new file mode 100644 index 00000000..e51400ef --- /dev/null +++ b/lib/olm/errors.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +//! All errors that could be caused by an operation regarding an `QOlmAccount`. +//! Errors are named exactly like the ones in libolm. +enum OlmAccountError { + BadAccountKey, + BadMessageKeyId, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + Unknown, +}; + +//! All errors that could be caused by an operation regarding an `QOlmSession`. +//! Errors are named exactly like the ones in libolm. +enum OlmSessionError { + BadAccountKey, + BadMessageFormat, + BadMessageKeyId, + BadMessageMac, + BadMessageVersion, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + Unknown, +}; + +//! All errors that could be caused by an operation +//! regarding QOlmOutboundGroupSession and QOlmInboundGroupSession. +//! Errors are named exactly like the ones in libolm. +enum OlmGroupSessionError { + BadAccountKey, + BadMessageFormat, + BadMessageMac, + BadMessageVersion, + BadSessionKey, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + UnknownMessageIndex, + Unknown, +}; diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp new file mode 100644 index 00000000..fbcaa802 --- /dev/null +++ b/lib/olm/qolminboundsession.cpp @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +std::variant QOlmInboundGroupSession::create(const QString &key) +{ + auto olmInboundGroupSessionBuf = QByteArray(olm_inbound_group_session_size(), '0'); + + const auto olmInboundGroupSession = olm_inbound_group_session(olmInboundGroupSessionBuf.data()); + + QByteArray keyBuf = key.toUtf8(); + + const auto error = olm_init_inbound_group_session(olmInboundGroupSession, keyBuf.data(), keyBuf.size()); + + if (error == olm_error()) { + return + } + + if create_error == errors::olm_error() { + Err(Self::last_error(olm_inbound_group_session_ptr)) + } else { + Ok(OlmInboundGroupSession { + group_session_ptr: olm_inbound_group_session_ptr, + group_session_buf: olm_inbound_group_session_buf, + }) + } + +} diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h new file mode 100644 index 00000000..520f8b68 --- /dev/null +++ b/lib/olm/qolminboundsession.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +//! An in-bound group session is responsible for decrypting incoming +//! communication in a Megolm session. +struct QOlmInboundGroupSession +{ +public: + //! Creates a new instance of `OlmInboundGroupSession`. + static std::variant create(const QString &key); +private: + OlmInboundGroupSession *m_groupSession + QByteArray m_buffer; +}; + -- cgit v1.2.3 From c8d67f737e84bbec98a54fc19a8aa56dbc39d542 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 23 Jan 2021 21:46:26 +0100 Subject: Implement Inboundsession --- lib/olm/e2ee.h | 71 +++++++++++++++++++ lib/olm/errors.cpp | 17 +++++ lib/olm/errors.h | 39 +++------- lib/olm/qolmaccount.cpp | 41 ++++++----- lib/olm/qolmaccount.h | 61 ++++------------ lib/olm/qolminboundsession.cpp | 157 ++++++++++++++++++++++++++++++++++++++--- lib/olm/qolminboundsession.h | 29 +++++++- 7 files changed, 307 insertions(+), 108 deletions(-) create mode 100644 lib/olm/e2ee.h create mode 100644 lib/olm/errors.cpp diff --git a/lib/olm/e2ee.h b/lib/olm/e2ee.h new file mode 100644 index 00000000..40ab56c7 --- /dev/null +++ b/lib/olm/e2ee.h @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "util.h" +#include +#include +#include +#include + +#include + +namespace Quotient { +inline const auto CiphertextKeyL = "ciphertext"_ls; +inline const auto SenderKeyKeyL = "sender_key"_ls; +inline const auto DeviceIdKeyL = "device_id"_ls; +inline const auto SessionIdKeyL = "session_id"_ls; + +inline const auto AlgorithmKeyL = "algorithm"_ls; +inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +inline const auto AlgorithmKey = QStringLiteral("algorithm"); +inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); +inline const auto RotationPeriodMsgsKey = + QStringLiteral("rotation_period_msgs"); + +inline const auto Ed25519Key = QStringLiteral("ed25519"); +inline const auto Curve25519Key = QStringLiteral("curve25519"); +inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); +inline const auto OlmV1Curve25519AesSha2AlgoKey = + QStringLiteral("m.olm.v1.curve25519-aes-sha2"); +inline const auto MegolmV1AesSha2AlgoKey = + QStringLiteral("m.megolm.v1.aes-sha2"); +inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; + +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant; + +template struct overloaded : Ts... { using Ts::operator()...; }; +template overloaded(Ts...) -> overloaded; + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the the one-time keys. +struct OneTimeKeys +{ + QMap> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QMap curve25519() const; + + //! Get a reference to the hashmap corresponding to given key type. + std::optional> get(QString keyType) const; +}; + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); + +} // namespace Quotient diff --git a/lib/olm/errors.cpp b/lib/olm/errors.cpp new file mode 100644 index 00000000..fce177c6 --- /dev/null +++ b/lib/olm/errors.cpp @@ -0,0 +1,17 @@ +#include "olm/errors.h" + +Quotient::OlmError Quotient::fromString(const std::string &error_raw) { + if (error_raw.compare("BAD_ACCOUNT_KEY")) { + return OlmError::BadAccountKey; + } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { + return OlmError::BadMessageKeyId; + } else if (error_raw.compare("INVALID_BASE64")) { + return OlmError::InvalidBase64; + } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { + return OlmError::NotEnoughRandom; + } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + return OlmError::OutputBufferTooSmall; + } else { + return OlmError::Unknown; + } +} diff --git a/lib/olm/errors.h b/lib/olm/errors.h index e51400ef..fc2ae2e9 100644 --- a/lib/olm/errors.h +++ b/lib/olm/errors.h @@ -2,22 +2,16 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#pragma once +#ifndef QUOTIENT_OLM_ERROR_H +#define QUOTIENT_OLM_ERROR_H -//! All errors that could be caused by an operation regarding an `QOlmAccount`. -//! Errors are named exactly like the ones in libolm. -enum OlmAccountError { - BadAccountKey, - BadMessageKeyId, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, - Unknown, -}; +#include -//! All errors that could be caused by an operation regarding an `QOlmSession`. +namespace Quotient { +//! All errors that could be caused by an operation regarding Olm //! Errors are named exactly like the ones in libolm. -enum OlmSessionError { +enum OlmError +{ BadAccountKey, BadMessageFormat, BadMessageKeyId, @@ -26,21 +20,10 @@ enum OlmSessionError { InvalidBase64, NotEnoughRandom, OutputBufferTooSmall, - Unknown, -}; - -//! All errors that could be caused by an operation -//! regarding QOlmOutboundGroupSession and QOlmInboundGroupSession. -//! Errors are named exactly like the ones in libolm. -enum OlmGroupSessionError { - BadAccountKey, - BadMessageFormat, - BadMessageMac, - BadMessageVersion, - BadSessionKey, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, UnknownMessageIndex, Unknown, }; + +} //namespace Quotient + +#endif diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index a6a07962..bde9b712 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -9,6 +9,8 @@ #include #include +using namespace Quotient; + QMap OneTimeKeys::curve25519() const { return keys[QStringLiteral("curve25519")]; @@ -23,7 +25,7 @@ std::optional> OneTimeKeys::get(QString keyType) const } // Convert PicklingMode to key -QByteArray toKey(PicklingMode mode) +QByteArray toKey(const PicklingMode &mode) { if (std::holds_alternative(mode)) { return ""; @@ -36,25 +38,30 @@ bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; } -// Conver olm error to enum -QOlmAccount::OlmAccountError lastError(OlmAccount *account) { - const std::string error_raw = olm_account_last_error(account); - +// TODO use impl from errors.cpp +OlmError fromString(const std::string &error_raw) { if (error_raw.compare("BAD_ACCOUNT_KEY")) { - return QOlmAccount::OlmAccountError::BadAccountKey; + return OlmError::BadAccountKey; } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { - return QOlmAccount::OlmAccountError::BadMessageKeyId; + return OlmError::BadMessageKeyId; } else if (error_raw.compare("INVALID_BASE64")) { - return QOlmAccount::OlmAccountError::InvalidBase64; + return OlmError::InvalidBase64; } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { - return QOlmAccount::OlmAccountError::NotEnoughRandom; + return OlmError::NotEnoughRandom; } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { - return QOlmAccount::OlmAccountError::OutputBufferTooSmall; + return OlmError::OutputBufferTooSmall; } else { - return QOlmAccount::OlmAccountError::Unknown; + return OlmError::Unknown; } } +// Conver olm error to enum +OlmError lastError(OlmAccount *account) { + const std::string error_raw = olm_account_last_error(account); + + return fromString(error_raw); +} + QByteArray getRandom(size_t bufferSize) { QByteArray buffer(bufferSize, '0'); @@ -83,7 +90,7 @@ std::optional QOlmAccount::create() return std::make_optional(account); } -std::variant QOlmAccount::unpickle(QByteArray pickled, PicklingMode mode) +std::variant QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) { auto account = olm_account(new uint8_t[olm_account_size()]); const QByteArray key = toKey(mode); @@ -94,7 +101,7 @@ std::variant QOlmAccount::unpickle(QB return QOlmAccount(account); } -std::variant QOlmAccount::pickle(PicklingMode mode) +std::variant QOlmAccount::pickle(const PicklingMode &mode) { const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); @@ -107,7 +114,7 @@ std::variant QOlmAccount::pickle(Pickl return pickleBuffer; } -std::variant QOlmAccount::identityKeys() +std::variant QOlmAccount::identityKeys() { const size_t keyLength = olm_account_identity_keys_length(m_account); QByteArray keyBuffer(keyLength, '0'); @@ -122,7 +129,7 @@ std::variant QOlmAccount::identityKe }; } -std::variant QOlmAccount::sign(QString message) const +std::variant QOlmAccount::sign(const QString &message) const { const size_t signatureLength = olm_account_signature_length(m_account); QByteArray signatureBuffer(signatureLength, '0'); @@ -140,7 +147,7 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const return olm_account_max_number_of_one_time_keys(m_account); } -std::optional QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +std::optional QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const { const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); QByteArray randomBuffer = getRandom(randomLen); @@ -152,7 +159,7 @@ std::optional QOlmAccount::generateOneTimeKeys(siz return std::nullopt; } -std::variant QOlmAccount::oneTimeKeys() const +std::variant QOlmAccount::oneTimeKeys() const { const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h index 268cd5d5..3ce1fb9a 100644 --- a/lib/olm/qolmaccount.h +++ b/lib/olm/qolmaccount.h @@ -3,44 +3,14 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include -#include -#include -#include -#include +#include "olm/e2ee.h" +#include "olm/errors.h" #include "olm/olm.h" +#include struct OlmAccount; -struct Unencrypted {}; -struct Encrypted { - QByteArray key; -}; - -using PicklingMode = std::variant; - -template struct overloaded : Ts... { using Ts::operator()...; }; -template overloaded(Ts...) -> overloaded; - -struct IdentityKeys -{ - QByteArray curve25519; - QByteArray ed25519; -}; - -//! Struct representing the the one-time keys. -struct OneTimeKeys -{ - QMap> keys; - - //! Get the HashMap containing the curve25519 one-time keys. - QMap curve25519() const; - - //! Get a reference to the hashmap corresponding to given key type. - std::optional> get(QString keyType) const; -}; - -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); +namespace Quotient { //! An olm account manages all cryptographic keys used on a device. //! \code{.cpp} @@ -51,41 +21,34 @@ class QOlmAccount public: ~QOlmAccount(); - enum OlmAccountError { - BadAccountKey, - BadMessageKeyId, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, - Unknown, - }; - //! Creates a new instance of OlmAccount. During the instantiation //! the Ed25519 fingerprint key pair and the Curve25519 identity key //! pair are generated. For more information see here. static std::optional create(); - static std::variant unpickle(QByteArray picked, PicklingMode mode); + static std::variant unpickle(QByteArray &picked, const PicklingMode &mode); //! Serialises an OlmAccount to encrypted Base64. - std::variant pickle(PicklingMode mode); - std::variant identityKeys(); + std::variant pickle(const PicklingMode &mode); + std::variant identityKeys(); //! Returns the signature of the supplied message. - std::variant sign(QString message) const; + std::variant sign(const QString &message) const; //! Maximum number of one time keys that this OlmAccount can //! currently hold. size_t maxNumberOfOneTimeKeys() const; //! Generates the supplied number of one time keys. - std::optional generateOneTimeKeys(size_t numberOfKeys) const; + std::optional generateOneTimeKeys(size_t numberOfKeys) const; //! Gets the OlmAccount's one time keys formatted as JSON. - std::variant oneTimeKeys() const; + std::variant oneTimeKeys() const; // HACK do not use directly QOlmAccount(OlmAccount *account); private: OlmAccount *m_account; }; + +} // namespace Quotient diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp index fbcaa802..62de138f 100644 --- a/lib/olm/qolminboundsession.cpp +++ b/lib/olm/qolminboundsession.cpp @@ -2,9 +2,42 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include "olm/qolminboundsession.h" +#include +#include -std::variant QOlmInboundGroupSession::create(const QString &key) +using namespace Quotient; + +// TODO move to errors.cpp +OlmError fromString(const std::string &error_raw) { + if (error_raw.compare("BAD_ACCOUNT_KEY")) { + return OlmError::BadAccountKey; + } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { + return OlmError::BadMessageKeyId; + } else if (error_raw.compare("INVALID_BASE64")) { + return OlmError::InvalidBase64; + } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { + return OlmError::NotEnoughRandom; + } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + return OlmError::OutputBufferTooSmall; + } else { + return OlmError::Unknown; + } +} + +OlmError lastError(OlmInboundGroupSession *session) { + const std::string error_raw = olm_inbound_group_session_last_error(session); + + return fromString(error_raw); +} + +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session, const QByteArray &buffer) + : m_groupSession(session) + , m_buffer(buffer) +{ +} + +std::variant QOlmInboundGroupSession::create(const QString &key) { auto olmInboundGroupSessionBuf = QByteArray(olm_inbound_group_session_size(), '0'); @@ -12,19 +45,121 @@ std::variant QOlmInboundGroupSe QByteArray keyBuf = key.toUtf8(); - const auto error = olm_init_inbound_group_session(olmInboundGroupSession, keyBuf.data(), keyBuf.size()); + const auto error = olm_init_inbound_group_session(olmInboundGroupSession, + reinterpret_cast(keyBuf.data()), keyBuf.size()); if (error == olm_error()) { - return + return lastError(olmInboundGroupSession); } - if create_error == errors::olm_error() { - Err(Self::last_error(olm_inbound_group_session_ptr)) - } else { - Ok(OlmInboundGroupSession { - group_session_ptr: olm_inbound_group_session_ptr, - group_session_buf: olm_inbound_group_session_buf, - }) + return QOlmInboundGroupSession(olmInboundGroupSession, olmInboundGroupSessionBuf); +} + + +std::variant QOlmInboundGroupSession::import(const QString &key) +{ + auto olmInboundGroupSessionBuf = QByteArray(olm_inbound_group_session_size(), '0'); + const auto olmInboundGroupSession = olm_inbound_group_session(olmInboundGroupSessionBuf.data()); + QByteArray keyBuf = key.toUtf8(); + + const auto error = olm_import_inbound_group_session(olmInboundGroupSession, + reinterpret_cast(keyBuf.data()), keyBuf.size()); + if (error == olm_error()) { + return lastError(olmInboundGroupSession); + } + + return QOlmInboundGroupSession(olmInboundGroupSession, olmInboundGroupSessionBuf); +} + +QByteArray toKey(const PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +std::variant QOlmInboundGroupSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); + const QByteArray key = toKey(mode); + const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), + pickledBuf.length()); + if (error == olm_error()) { + return lastError(m_groupSession); } + return pickledBuf; +} + +std::variant QOlmInboundGroupSession::unpicke(QByteArray &picked, const PicklingMode &mode) +{ + QByteArray groupSessionBuf(olm_inbound_group_session_size(), '0'); + auto groupSession = olm_inbound_group_session(groupSessionBuf.data()); + const QByteArray key = toKey(mode); + const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), picked.data(), picked.size()); + if (error == olm_error()) { + return lastError(groupSession); + } + return QOlmInboundGroupSession(groupSession, groupSessionBuf); +} + +std::variant, OlmError> QOlmInboundGroupSession::decrypt(QString &message) +{ + // This is for capturing the output of olm_group_decrypt + uint32_t messageIndex = 0; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf = message.toUtf8(); + + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, + reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); + const auto messageLen = messageBuf.length(); + const auto plaintextMaxLen = plaintextBuf.length(); + + const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), + messageLen, reinterpret_cast(plaintextBuf.data()), plaintextMaxLen, &messageIndex); + + // Error code or plaintext length is returned + const auto decryptError = plaintextLen; + + if (decryptError == olm_error()) { + return lastError(m_groupSession); + } + + plaintextBuf.truncate(plaintextLen); + return std::make_pair(QString(plaintextBuf), messageIndex); +} +std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +{ + const auto keyLen = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLen, '0'); + const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLen, messageIndex); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuf; +} + +uint32_t QOlmInboundGroupSession::firstKnownIndex() const +{ + return olm_inbound_group_session_first_known_index(m_groupSession); +} + +std::variant QOlmInboundGroupSession::sessionId() const +{ + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); + const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), + sessionIdBuf.length()); + if (error == olm_error()) { + return lastError(m_groupSession); + } + return sessionIdBuf; +} + +bool QOlmInboundGroupSession::isVerified() const +{ + return olm_inbound_group_session_is_verified(m_groupSession) != 0; } diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h index 520f8b68..c75b7285 100644 --- a/lib/olm/qolminboundsession.h +++ b/lib/olm/qolminboundsession.h @@ -6,6 +6,11 @@ #include #include +#include "olm/olm.h" +#include "olm/errors.h" +#include "olm/e2ee.h" + +namespace Quotient { //! An in-bound group session is responsible for decrypting incoming //! communication in a Megolm session. @@ -13,9 +18,27 @@ struct QOlmInboundGroupSession { public: //! Creates a new instance of `OlmInboundGroupSession`. - static std::variant create(const QString &key); + static std::variant create(const QString &key); + //! Import an inbound group session, from a previous export. + static std::variant import(const QString &key); + //! Serialises an `OlmInboundGroupSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by pickling + //! an `OlmInboundGroupSession`. + static std::variant unpicke(QByteArray &picked, const PicklingMode &mode); + //! Decrypts ciphertext received for this group session. + std::variant, OlmError> decrypt(QString &message); + //! Export the base64-encoded ratchet key for this session, at the given index, + //! in a format which can be used by import. + std::variant exportSession(uint32_t messageIndex); + //! Get the first message index we know how to decrypt. + uint32_t firstKnownIndex() const; + //! Get a base64-encoded identifier for this session. + std::variant sessionId() const; + bool isVerified() const; private: - OlmInboundGroupSession *m_groupSession + QOlmInboundGroupSession(OlmInboundGroupSession *session, const QByteArray &buffer); + OlmInboundGroupSession *m_groupSession; QByteArray m_buffer; }; - +} // namespace Quotient -- cgit v1.2.3 From 8706c055e69b01097b702403aaa0d222e5ab0d29 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 01:45:43 +0100 Subject: Implement outboundsession --- CMakeLists.txt | 4 ++ autotests/CMakeLists.txt | 1 + autotests/testgroupsession.cpp | 39 +++++++++++++ autotests/testgroupsession.h | 15 +++++ autotests/testolmaccount.cpp | 2 + lib/olm/errors.h | 2 + lib/olm/qolmaccount.cpp | 27 +-------- lib/olm/qolminboundsession.cpp | 51 +++++++---------- lib/olm/qolminboundsession.h | 9 +-- lib/olm/qolmoutboundsession.cpp | 121 ++++++++++++++++++++++++++++++++++++++++ lib/olm/qolmoutboundsession.h | 47 ++++++++++++++++ lib/olm/utils.cpp | 22 ++++++++ lib/olm/utils.h | 13 +++++ 13 files changed, 291 insertions(+), 62 deletions(-) create mode 100644 autotests/testgroupsession.cpp create mode 100644 autotests/testgroupsession.h create mode 100644 lib/olm/qolmoutboundsession.cpp create mode 100644 lib/olm/qolmoutboundsession.h create mode 100644 lib/olm/utils.cpp create mode 100644 lib/olm/utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b4d4ef8b..18a7b622 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,10 @@ list(APPEND lib_SRCS lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp lib/olm/qolmaccount.cpp + lib/olm/qolminboundsession.cpp + lib/olm/qolmoutboundsession.cpp + lib/olm/utils.cpp + lib/olm/errors.cpp ) # Configure API files generation diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 07c22ad6..31cdb446 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -13,3 +13,4 @@ endfunction() quotient_add_test(NAME callcandidateseventtest) quotient_add_test(NAME testolmaccount) +quotient_add_test(NAME testgroupsession) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp new file mode 100644 index 00000000..02892366 --- /dev/null +++ b/autotests/testgroupsession.cpp @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "testgroupsession.h" +#include "olm/qolminboundsession.h" +#include "olm/qolmoutboundsession.h" +#include "olm/utils.h" + +using namespace Quotient; + +void TestOlmSession::groupSessionPicklingValid() +{ + auto ogs = std::get(QOlmOutboundGroupSession::create()); + const auto ogsId = std::get(ogs.sessionId()); + QVERIFY(QByteArray::fromBase64Encoding(ogsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QCOMPARE(0, ogs.sessionMessageIndex()); + + auto ogsPickled = std::get(ogs.pickle(Unencrypted {})); + ogs = std::get(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {})); + QCOMPARE(ogsId, std::get(ogs.sessionId())); + + qDebug() << std::get(ogs.sessionKey()); + auto igs = std::get(QOlmInboundGroupSession::create(std::get(ogs.sessionKey()))); + const auto igsId = std::get(igs.sessionId()); + // ID is valid base64? + QVERIFY(QByteArray::fromBase64Encoding(igsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + + //// no messages have been sent yet + QCOMPARE(0, igs.firstKnownIndex()); + + auto igsPickled = std::get(igs.pickle(Unencrypted {})); + igs = std::get(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {})); + QCOMPARE(igsId, std::get(igs.sessionId())); +} + +QTEST_MAIN(TestOlmSession) +#endif diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h new file mode 100644 index 00000000..28ebf4c9 --- /dev/null +++ b/autotests/testgroupsession.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include + +class TestOlmSession : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void groupSessionPicklingValid(); +}; +#endif diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 549f07ea..45a7e3a5 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -6,6 +6,8 @@ #include "testolmaccount.h" #include "olm/qolmaccount.h" +using namespace Quotient; + void TestOlmAccount::pickleUnpickedTest() { auto olmAccount = QOlmAccount::create().value(); diff --git a/lib/olm/errors.h b/lib/olm/errors.h index fc2ae2e9..3dc4c796 100644 --- a/lib/olm/errors.h +++ b/lib/olm/errors.h @@ -24,6 +24,8 @@ enum OlmError Unknown, }; +OlmError fromString(const std::string &error_raw); + } //namespace Quotient #endif diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index bde9b712..89d82832 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -4,6 +4,7 @@ #ifdef Quotient_E2EE_ENABLED #include "qolmaccount.h" +#include "olm/utils.h" #include #include #include @@ -24,37 +25,11 @@ std::optional> OneTimeKeys::get(QString keyType) const return keys[keyType]; } -// Convert PicklingMode to key -QByteArray toKey(const PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return ""; - } - return std::get(mode).key; -} - bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) { return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; } -// TODO use impl from errors.cpp -OlmError fromString(const std::string &error_raw) { - if (error_raw.compare("BAD_ACCOUNT_KEY")) { - return OlmError::BadAccountKey; - } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { - return OlmError::BadMessageKeyId; - } else if (error_raw.compare("INVALID_BASE64")) { - return OlmError::InvalidBase64; - } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { - return OlmError::NotEnoughRandom; - } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { - return OlmError::OutputBufferTooSmall; - } else { - return OlmError::Unknown; - } -} - // Conver olm error to enum OlmError lastError(OlmAccount *account) { const std::string error_raw = olm_account_last_error(account); diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp index 62de138f..37dd60f8 100644 --- a/lib/olm/qolminboundsession.cpp +++ b/lib/olm/qolminboundsession.cpp @@ -3,64 +3,51 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "olm/qolminboundsession.h" -#include -#include - +#include +#include using namespace Quotient; -// TODO move to errors.cpp -OlmError fromString(const std::string &error_raw) { - if (error_raw.compare("BAD_ACCOUNT_KEY")) { - return OlmError::BadAccountKey; - } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { - return OlmError::BadMessageKeyId; - } else if (error_raw.compare("INVALID_BASE64")) { - return OlmError::InvalidBase64; - } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { - return OlmError::NotEnoughRandom; - } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { - return OlmError::OutputBufferTooSmall; - } else { - return OlmError::Unknown; - } -} - OlmError lastError(OlmInboundGroupSession *session) { const std::string error_raw = olm_inbound_group_session_last_error(session); + std::cout << error_raw; return fromString(error_raw); } -QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session, const QByteArray &buffer) +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session, QByteArray buffer) : m_groupSession(session) , m_buffer(buffer) { } -std::variant QOlmInboundGroupSession::create(const QString &key) +QOlmInboundGroupSession::~QOlmInboundGroupSession() { - auto olmInboundGroupSessionBuf = QByteArray(olm_inbound_group_session_size(), '0'); + olm_clear_inbound_group_session(m_groupSession); +} +std::variant QOlmInboundGroupSession::create(const QByteArray &key) +{ + QByteArray olmInboundGroupSessionBuf(olm_inbound_group_session_size(), '0'); const auto olmInboundGroupSession = olm_inbound_group_session(olmInboundGroupSessionBuf.data()); - QByteArray keyBuf = key.toUtf8(); + const auto temp = key; const auto error = olm_init_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(keyBuf.data()), keyBuf.size()); + reinterpret_cast(temp.data()), temp.size()); if (error == olm_error()) { return lastError(olmInboundGroupSession); } - return QOlmInboundGroupSession(olmInboundGroupSession, olmInboundGroupSessionBuf); + return QOlmInboundGroupSession(olmInboundGroupSession, std::move(olmInboundGroupSessionBuf)); } -std::variant QOlmInboundGroupSession::import(const QString &key) +std::variant QOlmInboundGroupSession::import(const QByteArray &key) { - auto olmInboundGroupSessionBuf = QByteArray(olm_inbound_group_session_size(), '0'); + QByteArray olmInboundGroupSessionBuf(olm_inbound_group_session_size(), '0'); const auto olmInboundGroupSession = olm_inbound_group_session(olmInboundGroupSessionBuf.data()); - QByteArray keyBuf = key.toUtf8(); + QByteArray keyBuf = key; const auto error = olm_import_inbound_group_session(olmInboundGroupSession, reinterpret_cast(keyBuf.data()), keyBuf.size()); @@ -68,7 +55,7 @@ std::variant QOlmInboundGroupSession::import( return lastError(olmInboundGroupSession); } - return QOlmInboundGroupSession(olmInboundGroupSession, olmInboundGroupSessionBuf); + return QOlmInboundGroupSession(olmInboundGroupSession, std::move(olmInboundGroupSessionBuf)); } QByteArray toKey(const PicklingMode &mode) @@ -91,7 +78,7 @@ std::variant QOlmInboundGroupSession::pickle(const Picklin return pickledBuf; } -std::variant QOlmInboundGroupSession::unpicke(QByteArray &picked, const PicklingMode &mode) +std::variant QOlmInboundGroupSession::unpickle(QByteArray &picked, const PicklingMode &mode) { QByteArray groupSessionBuf(olm_inbound_group_session_size(), '0'); auto groupSession = olm_inbound_group_session(groupSessionBuf.data()); @@ -100,7 +87,7 @@ std::variant QOlmInboundGroupSession::unpicke if (error == olm_error()) { return lastError(groupSession); } - return QOlmInboundGroupSession(groupSession, groupSessionBuf); + return QOlmInboundGroupSession(groupSession, std::move(groupSessionBuf)); } std::variant, OlmError> QOlmInboundGroupSession::decrypt(QString &message) diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h index c75b7285..82802520 100644 --- a/lib/olm/qolminboundsession.h +++ b/lib/olm/qolminboundsession.h @@ -17,15 +17,16 @@ namespace Quotient { struct QOlmInboundGroupSession { public: + ~QOlmInboundGroupSession(); //! Creates a new instance of `OlmInboundGroupSession`. - static std::variant create(const QString &key); + static std::variant create(const QByteArray &key); //! Import an inbound group session, from a previous export. - static std::variant import(const QString &key); + static std::variant import(const QByteArray &key); //! Serialises an `OlmInboundGroupSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant unpicke(QByteArray &picked, const PicklingMode &mode); + static std::variant unpickle(QByteArray &picked, const PicklingMode &mode); //! Decrypts ciphertext received for this group session. std::variant, OlmError> decrypt(QString &message); //! Export the base64-encoded ratchet key for this session, at the given index, @@ -37,7 +38,7 @@ public: std::variant sessionId() const; bool isVerified() const; private: - QOlmInboundGroupSession(OlmInboundGroupSession *session, const QByteArray &buffer); + QOlmInboundGroupSession(OlmInboundGroupSession *session, QByteArray buffer); OlmInboundGroupSession *m_groupSession; QByteArray m_buffer; }; diff --git a/lib/olm/qolmoutboundsession.cpp b/lib/olm/qolmoutboundsession.cpp new file mode 100644 index 00000000..8a6b966b --- /dev/null +++ b/lib/olm/qolmoutboundsession.cpp @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later +// +#include "olm/qolmoutboundsession.h" +#include "olm/utils.h" + +using namespace Quotient; + +OlmError lastError(OlmOutboundGroupSession *session) { + const std::string error_raw = olm_outbound_group_session_last_error(session); + + return fromString(error_raw); +} + +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session, const QByteArray &buffer) + : m_groupSession(session) + , m_buffer(buffer) +{ +} + +QOlmOutboundGroupSession::~QOlmOutboundGroupSession() +{ + olm_clear_outbound_group_session(m_groupSession); +} + +std::variant QOlmOutboundGroupSession::create() +{ + QByteArray sessionBuffer(olm_outbound_group_session_size(), '0'); + auto *olmOutboundGroupSession = olm_outbound_group_session(sessionBuffer.data()); + const auto randomLen = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); + QByteArray randomBuf = getRandom(randomLen); + + const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + return lastError(olmOutboundGroupSession); + } + + randomBuf.clear(); + + return QOlmOutboundGroupSession(olmOutboundGroupSession, sessionBuffer); +} + +std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + key.clear(); + + return pickledBuf; +} + + +std::variant QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + QByteArray olmOutboundGroupSessionBuf(olm_outbound_group_session_size(), '0'); + QByteArray key = toKey(mode); + auto olmOutboundGroupSession = olm_outbound_group_session(reinterpret_cast(olmOutboundGroupSessionBuf.data())); + const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), + pickled.data(), pickled.length()); + if (error == olm_error()) { + return lastError(olmOutboundGroupSession); + } + + key.clear(); + return QOlmOutboundGroupSession(olmOutboundGroupSession, olmOutboundGroupSessionBuf); +} + +std::variant QOlmOutboundGroupSession::encrypt(QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), + plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + return messageBuf; +} + +uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const +{ + return olm_outbound_group_session_message_index(m_groupSession); +} + +std::variant QOlmOutboundGroupSession::sessionId() const +{ + const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + return lastError(m_groupSession); + } + return idBuffer; +} + +std::variant QOlmOutboundGroupSession::sessionKey() const +{ + const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuffer; +} diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h new file mode 100644 index 00000000..147c0e03 --- /dev/null +++ b/lib/olm/qolmoutboundsession.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once + +#include "olm/olm.h" // from Olm +#include "olm/errors.h" +#include "olm/e2ee.h" + +namespace Quotient { + +//! An out-bound group session is responsible for encrypting outgoing +//! communication in a Megolm session. +class QOlmOutboundGroupSession +{ +public: + ~QOlmOutboundGroupSession(); + //! Creates a new instance of `QOlmOutboundGroupSession`. + static std::variant create(); + //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by + //! pickling an `QOlmOutboundGroupSession`. + static std::variant unpickle(QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + std::variant encrypt(QString &plaintext); + + //! Get the current message index for this session. + //! + //! Each message is sent with an increasing index; this returns the + //! index for the next message. + uint32_t sessionMessageIndex() const; + + //! Get a base64-encoded identifier for this session. + std::variant sessionId() const; + + //! Get the base64-encoded current ratchet key for this session. + //! + //! Each message is sent with a different ratchet key. This function returns the + //! ratchet key that will be used for the next message. + std::variant sessionKey() const; +private: + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession, const QByteArray &groupSessionBuf); + OlmOutboundGroupSession *m_groupSession; + QByteArray m_buffer; +}; +} diff --git a/lib/olm/utils.cpp b/lib/olm/utils.cpp new file mode 100644 index 00000000..4966af15 --- /dev/null +++ b/lib/olm/utils.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "olm/utils.h" + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +QByteArray Quotient::getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + std::generate(buffer.begin(), buffer.end(), std::rand); + return buffer; +} diff --git a/lib/olm/utils.h b/lib/olm/utils.h new file mode 100644 index 00000000..ec0da784 --- /dev/null +++ b/lib/olm/utils.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "olm/e2ee.h" + +namespace Quotient { +// Convert PicklingMode to key +QByteArray toKey(const PicklingMode &mode); +QByteArray getRandom(size_t bufferSize); +} -- cgit v1.2.3 From 5b072f7d2519df4c60cc6ae4ce728e99a09d8d44 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 17:59:31 +0100 Subject: ifdef everything --- lib/olm/errors.cpp | 4 ++++ lib/olm/errors.h | 4 ++-- lib/olm/qolmaccount.h | 3 +++ lib/olm/qolminboundsession.cpp | 2 ++ lib/olm/qolminboundsession.h | 3 +++ lib/olm/qolmoutboundsession.cpp | 5 ++++- lib/olm/qolmoutboundsession.h | 2 ++ lib/olm/utils.cpp | 2 ++ lib/olm/utils.h | 2 ++ 9 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/olm/errors.cpp b/lib/olm/errors.cpp index fce177c6..a687e807 100644 --- a/lib/olm/errors.cpp +++ b/lib/olm/errors.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later +#ifdef Quotient_E2EE_ENABLED #include "olm/errors.h" Quotient::OlmError Quotient::fromString(const std::string &error_raw) { @@ -15,3 +18,4 @@ Quotient::OlmError Quotient::fromString(const std::string &error_raw) { return OlmError::Unknown; } } +#endif diff --git a/lib/olm/errors.h b/lib/olm/errors.h index 3dc4c796..09d2a989 100644 --- a/lib/olm/errors.h +++ b/lib/olm/errors.h @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifndef QUOTIENT_OLM_ERROR_H -#define QUOTIENT_OLM_ERROR_H +#pragma once +#ifdef Quotient_E2EE_ENABLED #include namespace Quotient { diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h index 3ce1fb9a..c478c781 100644 --- a/lib/olm/qolmaccount.h +++ b/lib/olm/qolmaccount.h @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#ifdef Quotient_E2EE_ENABLED #include "olm/e2ee.h" #include "olm/errors.h" @@ -52,3 +53,5 @@ private: }; } // namespace Quotient + +#endif diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp index 37dd60f8..f0ca73c4 100644 --- a/lib/olm/qolminboundsession.cpp +++ b/lib/olm/qolminboundsession.cpp @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later +#ifdef Quotient_E2EE_ENABLED #include "olm/qolminboundsession.h" #include #include @@ -150,3 +151,4 @@ bool QOlmInboundGroupSession::isVerified() const { return olm_inbound_group_session_is_verified(m_groupSession) != 0; } +#endif diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h index 82802520..85807821 100644 --- a/lib/olm/qolminboundsession.h +++ b/lib/olm/qolminboundsession.h @@ -4,6 +4,8 @@ #pragma once +#ifdef Quotient_E2EE_ENABLED + #include #include #include "olm/olm.h" @@ -43,3 +45,4 @@ private: QByteArray m_buffer; }; } // namespace Quotient +#endif diff --git a/lib/olm/qolmoutboundsession.cpp b/lib/olm/qolmoutboundsession.cpp index 8a6b966b..60126469 100644 --- a/lib/olm/qolmoutboundsession.cpp +++ b/lib/olm/qolmoutboundsession.cpp @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan // // SPDX-License-Identifier: LGPL-2.1-or-later -// + +#ifdef Quotient_E2EE_ENABLED #include "olm/qolmoutboundsession.h" #include "olm/utils.h" @@ -119,3 +120,5 @@ std::variant QOlmOutboundGroupSession::sessionKey() const } return keyBuffer; } + +#endif diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h index 147c0e03..41eb874a 100644 --- a/lib/olm/qolmoutboundsession.h +++ b/lib/olm/qolmoutboundsession.h @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#ifdef Quotient_E2EE_ENABLED #include "olm/olm.h" // from Olm #include "olm/errors.h" @@ -45,3 +46,4 @@ private: QByteArray m_buffer; }; } +#endif diff --git a/lib/olm/utils.cpp b/lib/olm/utils.cpp index 4966af15..15def1d7 100644 --- a/lib/olm/utils.cpp +++ b/lib/olm/utils.cpp @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later +#ifdef Quotient_E2EE_ENABLED #include "olm/utils.h" using namespace Quotient; @@ -20,3 +21,4 @@ QByteArray Quotient::getRandom(size_t bufferSize) std::generate(buffer.begin(), buffer.end(), std::rand); return buffer; } +#endif diff --git a/lib/olm/utils.h b/lib/olm/utils.h index ec0da784..85d4605b 100644 --- a/lib/olm/utils.h +++ b/lib/olm/utils.h @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#ifdef Quotient_E2EE_ENABLED #include "olm/e2ee.h" @@ -11,3 +12,4 @@ namespace Quotient { QByteArray toKey(const PicklingMode &mode); QByteArray getRandom(size_t bufferSize); } +#endif -- cgit v1.2.3 From 0b2b70ac815f48fd83424c282fcf50ef97b08a7b Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 18:02:09 +0100 Subject: Fix documentation typos Co-authored-by: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> --- lib/olm/e2ee.h | 2 +- lib/olm/qolmaccount.cpp | 2 +- lib/olm/qolmoutboundsession.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/olm/e2ee.h b/lib/olm/e2ee.h index 40ab56c7..1dee0e42 100644 --- a/lib/olm/e2ee.h +++ b/lib/olm/e2ee.h @@ -54,7 +54,7 @@ struct IdentityKeys QByteArray ed25519; }; -//! Struct representing the the one-time keys. +//! Struct representing the one-time keys. struct OneTimeKeys { QMap> keys; diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index 89d82832..742d7d18 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -30,7 +30,7 @@ bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; } -// Conver olm error to enum +// Convert olm error to enum OlmError lastError(OlmAccount *account) { const std::string error_raw = olm_account_last_error(account); diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h index 41eb874a..2e1439d3 100644 --- a/lib/olm/qolmoutboundsession.h +++ b/lib/olm/qolmoutboundsession.h @@ -21,7 +21,7 @@ public: //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by - //! pickling an `QOlmOutboundGroupSession`. + //! pickling a `QOlmOutboundGroupSession`. static std::variant unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. std::variant encrypt(QString &plaintext); -- cgit v1.2.3 From faaea2eb2a090787f365eb12a42f8452bb0f07e2 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 20:12:39 +0100 Subject: Make it work --- lib/olm/qolminboundsession.cpp | 48 ++++++++++++++++++++--------------------- lib/olm/qolminboundsession.h | 14 ++++++------ lib/olm/qolmoutboundsession.cpp | 31 +++++++++++++++----------- lib/olm/qolmoutboundsession.h | 9 ++++---- lib/olm/utils.cpp | 4 +++- 5 files changed, 57 insertions(+), 49 deletions(-) diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp index f0ca73c4..d3b98a63 100644 --- a/lib/olm/qolminboundsession.cpp +++ b/lib/olm/qolminboundsession.cpp @@ -4,7 +4,6 @@ #ifdef Quotient_E2EE_ENABLED #include "olm/qolminboundsession.h" -#include #include using namespace Quotient; @@ -15,48 +14,44 @@ OlmError lastError(OlmInboundGroupSession *session) { return fromString(error_raw); } -QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session, QByteArray buffer) +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) : m_groupSession(session) - , m_buffer(buffer) { } QOlmInboundGroupSession::~QOlmInboundGroupSession() { olm_clear_inbound_group_session(m_groupSession); + //delete[](reinterpret_cast(m_groupSession)); } -std::variant QOlmInboundGroupSession::create(const QByteArray &key) +std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) { - QByteArray olmInboundGroupSessionBuf(olm_inbound_group_session_size(), '0'); - const auto olmInboundGroupSession = olm_inbound_group_session(olmInboundGroupSessionBuf.data()); - + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); const auto temp = key; - const auto error = olm_init_inbound_group_session(olmInboundGroupSession, reinterpret_cast(temp.data()), temp.size()); if (error == olm_error()) { - return lastError(olmInboundGroupSession); + throw lastError(olmInboundGroupSession); } - return QOlmInboundGroupSession(olmInboundGroupSession, std::move(olmInboundGroupSessionBuf)); + return std::make_unique(olmInboundGroupSession); } -std::variant QOlmInboundGroupSession::import(const QByteArray &key) +std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) { - QByteArray olmInboundGroupSessionBuf(olm_inbound_group_session_size(), '0'); - const auto olmInboundGroupSession = olm_inbound_group_session(olmInboundGroupSessionBuf.data()); + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); QByteArray keyBuf = key; const auto error = olm_import_inbound_group_session(olmInboundGroupSession, reinterpret_cast(keyBuf.data()), keyBuf.size()); if (error == olm_error()) { - return lastError(olmInboundGroupSession); + throw lastError(olmInboundGroupSession); } - return QOlmInboundGroupSession(olmInboundGroupSession, std::move(olmInboundGroupSessionBuf)); + return std::make_unique(olmInboundGroupSession); } QByteArray toKey(const PicklingMode &mode) @@ -67,28 +62,31 @@ QByteArray toKey(const PicklingMode &mode) return std::get(mode).key; } -std::variant QOlmInboundGroupSession::pickle(const PicklingMode &mode) const +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); const QByteArray key = toKey(mode); const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), pickledBuf.length()); if (error == olm_error()) { - return lastError(m_groupSession); + throw lastError(m_groupSession); } return pickledBuf; } -std::variant QOlmInboundGroupSession::unpickle(QByteArray &picked, const PicklingMode &mode) +std::variant, OlmError> QOlmInboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) { - QByteArray groupSessionBuf(olm_inbound_group_session_size(), '0'); - auto groupSession = olm_inbound_group_session(groupSessionBuf.data()); - const QByteArray key = toKey(mode); - const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), picked.data(), picked.size()); + QByteArray pickledBuf = pickled; + const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.size()); if (error == olm_error()) { return lastError(groupSession); } - return QOlmInboundGroupSession(groupSession, std::move(groupSessionBuf)); + key.clear(); + + return std::make_unique(groupSession); } std::variant, OlmError> QOlmInboundGroupSession::decrypt(QString &message) @@ -136,13 +134,13 @@ uint32_t QOlmInboundGroupSession::firstKnownIndex() const return olm_inbound_group_session_first_known_index(m_groupSession); } -std::variant QOlmInboundGroupSession::sessionId() const +QByteArray QOlmInboundGroupSession::sessionId() const { QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), sessionIdBuf.length()); if (error == olm_error()) { - return lastError(m_groupSession); + throw lastError(m_groupSession); } return sessionIdBuf; } diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h index 85807821..ccc53ba8 100644 --- a/lib/olm/qolminboundsession.h +++ b/lib/olm/qolminboundsession.h @@ -8,6 +8,7 @@ #include #include +#include #include "olm/olm.h" #include "olm/errors.h" #include "olm/e2ee.h" @@ -21,14 +22,14 @@ struct QOlmInboundGroupSession public: ~QOlmInboundGroupSession(); //! Creates a new instance of `OlmInboundGroupSession`. - static std::variant create(const QByteArray &key); + static std::unique_ptr create(const QByteArray &key); //! Import an inbound group session, from a previous export. - static std::variant import(const QByteArray &key); + static std::unique_ptr import(const QByteArray &key); //! Serialises an `OlmInboundGroupSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode) const; + QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant unpickle(QByteArray &picked, const PicklingMode &mode); + static std::variant, OlmError> unpickle(QByteArray &picked, const PicklingMode &mode); //! Decrypts ciphertext received for this group session. std::variant, OlmError> decrypt(QString &message); //! Export the base64-encoded ratchet key for this session, at the given index, @@ -37,12 +38,11 @@ public: //! Get the first message index we know how to decrypt. uint32_t firstKnownIndex() const; //! Get a base64-encoded identifier for this session. - std::variant sessionId() const; + QByteArray sessionId() const; bool isVerified() const; + QOlmInboundGroupSession(OlmInboundGroupSession *session); private: - QOlmInboundGroupSession(OlmInboundGroupSession *session, QByteArray buffer); OlmInboundGroupSession *m_groupSession; - QByteArray m_buffer; }; } // namespace Quotient #endif diff --git a/lib/olm/qolmoutboundsession.cpp b/lib/olm/qolmoutboundsession.cpp index 60126469..ba8be4f6 100644 --- a/lib/olm/qolmoutboundsession.cpp +++ b/lib/olm/qolmoutboundsession.cpp @@ -14,34 +14,38 @@ OlmError lastError(OlmOutboundGroupSession *session) { return fromString(error_raw); } -QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session, const QByteArray &buffer) +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) : m_groupSession(session) - , m_buffer(buffer) { } QOlmOutboundGroupSession::~QOlmOutboundGroupSession() { olm_clear_outbound_group_session(m_groupSession); + //delete[](reinterpret_cast(m_groupSession)); } -std::variant QOlmOutboundGroupSession::create() +std::unique_ptr QOlmOutboundGroupSession::create() { - QByteArray sessionBuffer(olm_outbound_group_session_size(), '0'); - auto *olmOutboundGroupSession = olm_outbound_group_session(sessionBuffer.data()); + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); const auto randomLen = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); QByteArray randomBuf = getRandom(randomLen); - const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, + const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, reinterpret_cast(randomBuf.data()), randomBuf.length()); if (error == olm_error()) { - return lastError(olmOutboundGroupSession); + throw lastError(olmOutboundGroupSession); } + const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); + randomBuf.clear(); - return QOlmOutboundGroupSession(olmOutboundGroupSession, sessionBuffer); + return std::make_unique(olmOutboundGroupSession); } std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) @@ -61,20 +65,23 @@ std::variant QOlmOutboundGroupSession::pickle(const Pickli } -std::variant QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +std::variant, OlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; - QByteArray olmOutboundGroupSessionBuf(olm_outbound_group_session_size(), '0'); + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); QByteArray key = toKey(mode); - auto olmOutboundGroupSession = olm_outbound_group_session(reinterpret_cast(olmOutboundGroupSessionBuf.data())); const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), pickled.data(), pickled.length()); if (error == olm_error()) { return lastError(olmOutboundGroupSession); } + const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); + QByteArray idBuffer(idMaxLength, '0'); + olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()); key.clear(); - return QOlmOutboundGroupSession(olmOutboundGroupSession, olmOutboundGroupSessionBuf); + return std::make_unique(olmOutboundGroupSession); } std::variant QOlmOutboundGroupSession::encrypt(QString &plaintext) diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h index 2e1439d3..29776a3d 100644 --- a/lib/olm/qolmoutboundsession.h +++ b/lib/olm/qolmoutboundsession.h @@ -7,6 +7,7 @@ #include "olm/olm.h" // from Olm #include "olm/errors.h" #include "olm/e2ee.h" +#include namespace Quotient { @@ -17,12 +18,13 @@ class QOlmOutboundGroupSession public: ~QOlmOutboundGroupSession(); //! Creates a new instance of `QOlmOutboundGroupSession`. - static std::variant create(); + //! Throw OlmError on errors + static std::unique_ptr create(); //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant unpickle(QByteArray &pickled, const PicklingMode &mode); + static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. std::variant encrypt(QString &plaintext); @@ -40,10 +42,9 @@ public: //! Each message is sent with a different ratchet key. This function returns the //! ratchet key that will be used for the next message. std::variant sessionKey() const; + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); private: - QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession, const QByteArray &groupSessionBuf); OlmOutboundGroupSession *m_groupSession; - QByteArray m_buffer; }; } #endif diff --git a/lib/olm/utils.cpp b/lib/olm/utils.cpp index 15def1d7..227e6d84 100644 --- a/lib/olm/utils.cpp +++ b/lib/olm/utils.cpp @@ -4,6 +4,8 @@ #ifdef Quotient_E2EE_ENABLED #include "olm/utils.h" +#include +#include using namespace Quotient; @@ -18,7 +20,7 @@ QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) QByteArray Quotient::getRandom(size_t bufferSize) { QByteArray buffer(bufferSize, '0'); - std::generate(buffer.begin(), buffer.end(), std::rand); + RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); return buffer; } #endif -- cgit v1.2.3 From 4e0fe24681daef069abde3448c26f5cb850d8bb7 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 20:13:16 +0100 Subject: Update test --- autotests/testgroupsession.cpp | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 02892366..1cfe38a9 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -12,27 +12,26 @@ using namespace Quotient; void TestOlmSession::groupSessionPicklingValid() { - auto ogs = std::get(QOlmOutboundGroupSession::create()); - const auto ogsId = std::get(ogs.sessionId()); + auto ogs = QOlmOutboundGroupSession::create(); + const auto ogsId = std::get(ogs->sessionId()); QVERIFY(QByteArray::fromBase64Encoding(ogsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); - QCOMPARE(0, ogs.sessionMessageIndex()); + QCOMPARE(0, ogs->sessionMessageIndex()); - auto ogsPickled = std::get(ogs.pickle(Unencrypted {})); - ogs = std::get(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {})); - QCOMPARE(ogsId, std::get(ogs.sessionId())); + auto ogsPickled = std::get(ogs->pickle(Unencrypted {})); + auto ogs2 = std::get>(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {})); + QCOMPARE(ogsId, std::get(ogs2->sessionId())); - qDebug() << std::get(ogs.sessionKey()); - auto igs = std::get(QOlmInboundGroupSession::create(std::get(ogs.sessionKey()))); - const auto igsId = std::get(igs.sessionId()); + auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); + const auto igsId = igs->sessionId(); // ID is valid base64? QVERIFY(QByteArray::fromBase64Encoding(igsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); //// no messages have been sent yet - QCOMPARE(0, igs.firstKnownIndex()); + QCOMPARE(0, igs->firstKnownIndex()); - auto igsPickled = std::get(igs.pickle(Unencrypted {})); - igs = std::get(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {})); - QCOMPARE(igsId, std::get(igs.sessionId())); + auto igsPickled = igs->pickle(Unencrypted {}); + igs = std::get>(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {})); + QCOMPARE(igsId, igs->sessionId()); } QTEST_MAIN(TestOlmSession) -- cgit v1.2.3 From 723038cb3fe16c3f0078d00362fcb53c10f9eb4a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 20:14:08 +0100 Subject: Depends on OpenSSL for crypo rand --- CMakeLists.txt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 18a7b622..a74d2b7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,13 @@ find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModul get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") +find_package(OpenSSL 1.1.0 REQUIRED) +set_package_properties(OpenSSL PROPERTIES + DESCRIPTION "Open source SSL and TLS implementation and cryptographic library" + URL "https://www.openssl.org/" + TYPE REQUIRED +) + if (${PROJECT_NAME}_ENABLE_E2EE) if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) @@ -325,7 +332,14 @@ if (${PROJECT_NAME}_ENABLE_E2EE) target_link_libraries(${PROJECT_NAME} Olm::Olm QtOlm) set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) + +target_link_libraries(${PROJECT_NAME} + ${Qt}::Core + ${Qt}::Network + ${Qt}::Gui + OpenSSL::Crypto + OpenSSL::SSL) + if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) endif() -- cgit v1.2.3 From dca69e8326ce6fd0374123ad8c167a2b0051d8fb Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 24 Jan 2021 20:54:30 +0100 Subject: Add group session decrypt/encrypt test and fix bug found by it --- autotests/testgroupsession.cpp | 27 +++++++++++++++++++++++---- autotests/testgroupsession.h | 1 + lib/olm/qolminboundsession.cpp | 19 ++++++++++++------- lib/olm/qolminboundsession.h | 4 +++- lib/olm/qolmoutboundsession.cpp | 6 +++--- lib/olm/qolmoutboundsession.h | 7 +++++-- 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 1cfe38a9..a99172d7 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -13,13 +13,13 @@ using namespace Quotient; void TestOlmSession::groupSessionPicklingValid() { auto ogs = QOlmOutboundGroupSession::create(); - const auto ogsId = std::get(ogs->sessionId()); + const auto ogsId = ogs->sessionId(); QVERIFY(QByteArray::fromBase64Encoding(ogsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); QCOMPARE(0, ogs->sessionMessageIndex()); auto ogsPickled = std::get(ogs->pickle(Unencrypted {})); - auto ogs2 = std::get>(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {})); - QCOMPARE(ogsId, std::get(ogs2->sessionId())); + auto ogs2 = std::get(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {})); + QCOMPARE(ogsId, ogs2->sessionId()); auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); const auto igsId = igs->sessionId(); @@ -30,9 +30,28 @@ void TestOlmSession::groupSessionPicklingValid() QCOMPARE(0, igs->firstKnownIndex()); auto igsPickled = igs->pickle(Unencrypted {}); - igs = std::get>(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {})); + igs = std::get(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {})); QCOMPARE(igsId, igs->sessionId()); } +void TestOlmSession::groupSessionCryptoValid() +{ + auto ogs = QOlmOutboundGroupSession::create(); + auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); + QCOMPARE(ogs->sessionId(), igs->sessionId()); + + const auto plainText = QStringLiteral("Hello world!"); + const auto ciphertext = std::get(ogs->encrypt(plainText)); + qDebug() << ciphertext; + // ciphertext valid base64? + QVERIFY(QByteArray::fromBase64Encoding(ciphertext).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + + const auto decryptionResult = std::get>(igs->decrypt(ciphertext)); + + //// correct plaintext? + QCOMPARE(plainText, decryptionResult.first); + + QCOMPARE(0, decryptionResult.second); +} QTEST_MAIN(TestOlmSession) #endif diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h index 28ebf4c9..c9192990 100644 --- a/autotests/testgroupsession.h +++ b/autotests/testgroupsession.h @@ -11,5 +11,6 @@ class TestOlmSession : public QObject private Q_SLOTS: void groupSessionPicklingValid(); + void groupSessionCryptoValid(); }; #endif diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp index d3b98a63..11558f51 100644 --- a/lib/olm/qolminboundsession.cpp +++ b/lib/olm/qolminboundsession.cpp @@ -5,6 +5,7 @@ #ifdef Quotient_E2EE_ENABLED #include "olm/qolminboundsession.h" #include +#include using namespace Quotient; OlmError lastError(OlmInboundGroupSession *session) { @@ -89,22 +90,24 @@ std::variant, OlmError> QOlmInboundGrou return std::make_unique(groupSession); } -std::variant, OlmError> QOlmInboundGroupSession::decrypt(QString &message) +std::variant, OlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) { // This is for capturing the output of olm_group_decrypt uint32_t messageIndex = 0; // We need to clone the message because // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf = message.toUtf8(); + QByteArray messageBuf(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); - const auto messageLen = messageBuf.length(); - const auto plaintextMaxLen = plaintextBuf.length(); + + messageBuf = QByteArray(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), - messageLen, reinterpret_cast(plaintextBuf.data()), plaintextMaxLen, &messageIndex); + messageBuf.length(), reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); // Error code or plaintext length is returned const auto decryptError = plaintextLen; @@ -113,8 +116,10 @@ std::variant, OlmError> QOlmInboundGroupSession::de return lastError(m_groupSession); } - plaintextBuf.truncate(plaintextLen); - return std::make_pair(QString(plaintextBuf), messageIndex); + QByteArray output(plaintextLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); + + return std::make_pair(QString(output), messageIndex); } std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h index ccc53ba8..eb698868 100644 --- a/lib/olm/qolminboundsession.h +++ b/lib/olm/qolminboundsession.h @@ -31,7 +31,7 @@ public: //! an `OlmInboundGroupSession`. static std::variant, OlmError> unpickle(QByteArray &picked, const PicklingMode &mode); //! Decrypts ciphertext received for this group session. - std::variant, OlmError> decrypt(QString &message); + std::variant, OlmError> decrypt(const QByteArray &message); //! Export the base64-encoded ratchet key for this session, at the given index, //! in a format which can be used by import. std::variant exportSession(uint32_t messageIndex); @@ -44,5 +44,7 @@ public: private: OlmInboundGroupSession *m_groupSession; }; + +using QOlmInboundGroupSessionPtr = std::unique_ptr; } // namespace Quotient #endif diff --git a/lib/olm/qolmoutboundsession.cpp b/lib/olm/qolmoutboundsession.cpp index ba8be4f6..4f3cc827 100644 --- a/lib/olm/qolmoutboundsession.cpp +++ b/lib/olm/qolmoutboundsession.cpp @@ -84,7 +84,7 @@ std::variant, OlmError> QOlmOutboundGr return std::make_unique(olmOutboundGroupSession); } -std::variant QOlmOutboundGroupSession::encrypt(QString &plaintext) +std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) { QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLen = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); @@ -104,14 +104,14 @@ uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const return olm_outbound_group_session_message_index(m_groupSession); } -std::variant QOlmOutboundGroupSession::sessionId() const +QByteArray QOlmOutboundGroupSession::sessionId() const { const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); QByteArray idBuffer(idMaxLength, '0'); const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), idBuffer.length()); if (error == olm_error()) { - return lastError(m_groupSession); + throw lastError(m_groupSession); } return idBuffer; } diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h index 29776a3d..a642f581 100644 --- a/lib/olm/qolmoutboundsession.h +++ b/lib/olm/qolmoutboundsession.h @@ -11,6 +11,7 @@ namespace Quotient { + //! An out-bound group session is responsible for encrypting outgoing //! communication in a Megolm session. class QOlmOutboundGroupSession @@ -26,7 +27,7 @@ public: //! pickling a `QOlmOutboundGroupSession`. static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. - std::variant encrypt(QString &plaintext); + std::variant encrypt(const QString &plaintext); //! Get the current message index for this session. //! @@ -35,7 +36,7 @@ public: uint32_t sessionMessageIndex() const; //! Get a base64-encoded identifier for this session. - std::variant sessionId() const; + QByteArray sessionId() const; //! Get the base64-encoded current ratchet key for this session. //! @@ -46,5 +47,7 @@ public: private: OlmOutboundGroupSession *m_groupSession; }; + +using QOlmOutboundGroupSessionPtr = std::unique_ptr; } #endif -- cgit v1.2.3 From 987d399bc9ce628c376d505e3ebb78ae703d7c68 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Tue, 26 Jan 2021 20:13:20 +0100 Subject: Improve API --- CMakeLists.txt | 1 + autotests/testolmaccount.cpp | 29 +++++++++------- lib/olm/e2ee.h | 11 ++++++ lib/olm/qolmaccount.cpp | 83 +++++++++++++++++++++++++++++++------------- lib/olm/qolmaccount.h | 35 ++++++++++++++----- lib/olm/qolmsession.cpp | 29 ++++++++++++++++ lib/olm/qolmsession.h | 49 ++++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 lib/olm/qolmsession.cpp create mode 100644 lib/olm/qolmsession.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a74d2b7b..a359ae07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,7 @@ list(APPEND lib_SRCS lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp lib/olm/qolmaccount.cpp + lib/olm/qolmsession.cpp lib/olm/qolminboundsession.cpp lib/olm/qolmoutboundsession.cpp lib/olm/utils.cpp diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 45a7e3a5..75102c32 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -10,19 +10,22 @@ using namespace Quotient; void TestOlmAccount::pickleUnpickedTest() { - auto olmAccount = QOlmAccount::create().value(); - auto identityKeys = std::get(olmAccount.identityKeys()); + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); + auto identityKeys = olmAccount.identityKeys(); auto pickled = std::get(olmAccount.pickle(Unencrypted{})); - auto olmAccount2 = std::get(QOlmAccount::unpickle(pickled, Unencrypted{})); - auto identityKeys2 = std::get(olmAccount2.identityKeys()); + QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount2.unpickle(pickled, Unencrypted{}); + auto identityKeys2 = olmAccount2.identityKeys(); QCOMPARE(identityKeys.curve25519, identityKeys2.curve25519); QCOMPARE(identityKeys.ed25519, identityKeys2.ed25519); } void TestOlmAccount::identityKeysValid() { - auto olmAccount = QOlmAccount::create().value(); - const auto identityKeys = std::get(olmAccount.identityKeys()); + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); + const auto identityKeys = olmAccount.identityKeys(); const auto curve25519 = identityKeys.curve25519; const auto ed25519 = identityKeys.ed25519; // verify encoded keys length @@ -36,10 +39,11 @@ void TestOlmAccount::identityKeysValid() void TestOlmAccount::signatureValid() { - const auto olmAccount = QOlmAccount::create().value(); + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); const auto message = "Hello world!"; - const auto signature = std::get(olmAccount.sign(message)); - QVERIFY(QByteArray::fromBase64Encoding(signature.toUtf8()).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + const auto signature = olmAccount.sign(message); + QVERIFY(QByteArray::fromBase64Encoding(signature).decodingStatus == QByteArray::Base64DecodingStatus::Ok); //let utility = OlmUtility::new(); //let identity_keys = olm_account.parsed_identity_keys(); @@ -51,15 +55,16 @@ void TestOlmAccount::signatureValid() void TestOlmAccount::oneTimeKeysValid() { - const auto olmAccount = QOlmAccount::create().value(); + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); const auto maxNumberOfOneTimeKeys = olmAccount.maxNumberOfOneTimeKeys(); QCOMPARE(100, maxNumberOfOneTimeKeys); - const auto oneTimeKeysEmpty = std::get(olmAccount.oneTimeKeys()); + const auto oneTimeKeysEmpty = olmAccount.oneTimeKeys(); QVERIFY(oneTimeKeysEmpty.curve25519().isEmpty()); olmAccount.generateOneTimeKeys(20); - const auto oneTimeKeysFilled = std::get(olmAccount.oneTimeKeys()); + const auto oneTimeKeysFilled = olmAccount.oneTimeKeys(); QCOMPARE(20, oneTimeKeysFilled.curve25519().count()); } diff --git a/lib/olm/e2ee.h b/lib/olm/e2ee.h index 1dee0e42..74f876e4 100644 --- a/lib/olm/e2ee.h +++ b/lib/olm/e2ee.h @@ -66,6 +66,17 @@ struct OneTimeKeys std::optional> get(QString keyType) const; }; +//! Struct representing the signed one-time keys. +struct SignedOneTimeKey +{ + //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. + QString key; + + //! Required. Signatures of the key object. + //! The signature is calculated using the process described at Signing JSON. + QMap> signatures; +}; + bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); } // namespace Quotient diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index 742d7d18..8872f66e 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -44,36 +44,37 @@ QByteArray getRandom(size_t bufferSize) return buffer; } -QOlmAccount::QOlmAccount(OlmAccount *account) - : m_account(account) -{} +QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId) + : m_userId(userId) + , m_deviceId(deviceId) +{ +} QOlmAccount::~QOlmAccount() { olm_clear_account(m_account); + delete[](reinterpret_cast(m_account)); } -std::optional QOlmAccount::create() +void QOlmAccount::createNewAccount() { - auto account = olm_account(new uint8_t[olm_account_size()]); - size_t randomSize = olm_create_account_random_length(account); + m_account = olm_account(new uint8_t[olm_account_size()]); + size_t randomSize = olm_create_account_random_length(m_account); QByteArray randomData = getRandom(randomSize); - const auto error = olm_create_account(account, randomData.data(), randomSize); + const auto error = olm_create_account(m_account, randomData.data(), randomSize); if (error == olm_error()) { - return std::nullopt; + throw lastError(m_account); } - return std::make_optional(account); } -std::variant QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) +void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) { - auto account = olm_account(new uint8_t[olm_account_size()]); + m_account = olm_account(new uint8_t[olm_account_size()]); const QByteArray key = toKey(mode); - const auto error = olm_unpickle_account(account, key.data(), key.length(), pickled.data(), pickled.size()); + const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); if (error == olm_error()) { - return lastError(account); + throw lastError(m_account); } - return QOlmAccount(account); } std::variant QOlmAccount::pickle(const PicklingMode &mode) @@ -89,13 +90,13 @@ std::variant QOlmAccount::pickle(const PicklingMode &mode) return pickleBuffer; } -std::variant QOlmAccount::identityKeys() +IdentityKeys QOlmAccount::identityKeys() const { const size_t keyLength = olm_account_identity_keys_length(m_account); QByteArray keyBuffer(keyLength, '0'); const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); if (error == olm_error()) { - return lastError(m_account); + throw lastError(m_account); } const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); return IdentityKeys { @@ -104,7 +105,7 @@ std::variant QOlmAccount::identityKeys() }; } -std::variant QOlmAccount::sign(const QString &message) const +QByteArray QOlmAccount::sign(const QByteArray &message) const { const size_t signatureLength = olm_account_signature_length(m_account); QByteArray signatureBuffer(signatureLength, '0'); @@ -112,9 +113,19 @@ std::variant QOlmAccount::sign(const QString &message) const signatureBuffer.data(), signatureLength); if (error == olm_error()) { - return lastError(m_account); + throw lastError(m_account); } - return QString::fromUtf8(signatureBuffer); + return signatureBuffer; +} + +QByteArray QOlmAccount::signIdentityKeys() const +{ + const auto keys = identityKeys(); + const QJsonObject j{ {Curve25519Key, QString(keys.curve25519)}, {Ed25519Key, QString(keys.ed25519)} }; + QJsonDocument doc; + doc.setObject(j); + return sign(doc.toJson()); + } size_t QOlmAccount::maxNumberOfOneTimeKeys() const @@ -122,26 +133,25 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const return olm_account_max_number_of_one_time_keys(m_account); } -std::optional QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +void QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const { const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); QByteArray randomBuffer = getRandom(randomLen); const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLen); if (error == olm_error()) { - return lastError(m_account); + throw lastError(m_account); } - return std::nullopt; } -std::variant QOlmAccount::oneTimeKeys() const +OneTimeKeys QOlmAccount::oneTimeKeys() const { const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); if (error == olm_error()) { - return lastError(m_account); + throw lastError(m_account); } const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); OneTimeKeys oneTimeKeys; @@ -157,4 +167,29 @@ std::variant QOlmAccount::oneTimeKeys() const return oneTimeKeys; } +QMap QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +{ + QMap signedOneTimeKeys; + for (const auto &keyid : keys.curve25519().keys()) { + const auto oneTimeKey = keys.curve25519()[keyid]; + QByteArray sign = signOneTimeKey(oneTimeKey); + signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); + } + return signedOneTimeKeys; +} + +SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const +{ + SignedOneTimeKey sign{}; + sign.key = key; + sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; + return sign; +} + +QByteArray QOlmAccount::signOneTimeKey(const QString &key) const +{ + QJsonDocument j(QJsonObject{{"key", key}}); + return sign(j.toJson()); +} + #endif diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h index c478c781..3b55212d 100644 --- a/lib/olm/qolmaccount.h +++ b/lib/olm/qolmaccount.h @@ -20,36 +20,53 @@ namespace Quotient { class QOlmAccount { public: + QOlmAccount(const QString &userId, const QString &deviceId); ~QOlmAccount(); //! Creates a new instance of OlmAccount. During the instantiation //! the Ed25519 fingerprint key pair and the Curve25519 identity key //! pair are generated. For more information see here. - static std::optional create(); - static std::variant unpickle(QByteArray &picked, const PicklingMode &mode); + //! This needs to be called before any other action or use unpickle() instead. + void createNewAccount(); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. + //! This needs to be called before any other action or use createNewAccount() instead. + void unpickle(QByteArray &picked, const PicklingMode &mode); //! Serialises an OlmAccount to encrypted Base64. std::variant pickle(const PicklingMode &mode); - std::variant identityKeys(); + + //! Returns the account's public identity keys already formatted as JSON + IdentityKeys identityKeys() const; //! Returns the signature of the supplied message. - std::variant sign(const QString &message) const; + QByteArray sign(const QByteArray &message) const; + + //! Sign identity keys. + QByteArray signIdentityKeys() const; //! Maximum number of one time keys that this OlmAccount can //! currently hold. size_t maxNumberOfOneTimeKeys() const; //! Generates the supplied number of one time keys. - std::optional generateOneTimeKeys(size_t numberOfKeys) const; + void generateOneTimeKeys(size_t numberOfKeys) const; //! Gets the OlmAccount's one time keys formatted as JSON. - std::variant oneTimeKeys() const; + OneTimeKeys oneTimeKeys() const; + + //! Sign all time key. + QMap signOneTimeKeys(const OneTimeKeys &keys) const; + + //! Sign one time key. + QByteArray signOneTimeKey(const QString &key) const; - // HACK do not use directly - QOlmAccount(OlmAccount *account); + SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; private: - OlmAccount *m_account; + OlmAccount *m_account = nullptr; + QString m_userId; + QString m_deviceId; }; } // namespace Quotient diff --git a/lib/olm/qolmsession.cpp b/lib/olm/qolmsession.cpp new file mode 100644 index 00000000..32a108a8 --- /dev/null +++ b/lib/olm/qolmsession.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "olm/qolmsession.h" + +using namespace Quotient; + +std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext) +{ + if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) { + return PreKeyMessage { ciphertext }; + } else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) { + return Message { ciphertext }; + } + return std::nullopt; +} + +std::pair toPair(const OlmMessage &message) +{ + return std::visit([](auto &arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::make_pair(MessageType, QByteArray(arg.message)); + } else if constexpr (std::is_same_v) { + return std::make_pair(PreKeyType, QByteArray(arg.message)); + } + }, message); +} diff --git a/lib/olm/qolmsession.h b/lib/olm/qolmsession.h new file mode 100644 index 00000000..08f47331 --- /dev/null +++ b/lib/olm/qolmsession.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "olm/e2ee.h" +#include "olm/olm.h" +#include "olm/errors.h" +#include + +namespace Quotient { + +//! An encrypted Olm message. +struct Message { + QByteArray message; +}; + +//! A encrypted Olm pre-key message. +//! +//! This message, unlike a normal Message, can be used to create new Olm sessions. +struct PreKeyMessage +{ + QByteArray message; +}; + +enum OlmMessageType +{ + PreKeyType, + MessageType, +}; + +using OlmMessage = std::variant; + +std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext); + +std::pair toPair(const OlmMessage &message); + +//class QOlmSession +//{ +// /// Creates an inbound session for sending/receiving messages from a received 'prekey' message. +// static std::variant, OlmError> createInboundSession(const QOlmAccount &account, +// PreKeyMessage &message); +// +////private: +// //static std::variant, OlmError> createSessionWith(std::function> func); +//} + +} -- cgit v1.2.3 From 799008ddbe2414ca0bce060a4d7f6a77c04c8d10 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Tue, 26 Jan 2021 04:39:04 +0300 Subject: E2EE: Introduce session (WiP) --- CMakeLists.txt | 2 + lib/olm/message.cpp | 35 ++++++++++ lib/olm/message.h | 46 +++++++++++++ lib/olm/qolmaccount.cpp | 5 ++ lib/olm/qolmaccount.h | 4 ++ lib/olm/qolminboundsession.h | 1 + lib/olm/qolmoutboundsession.h | 1 + lib/olm/session.cpp | 155 ++++++++++++++++++++++++++++++++++++++++++ lib/olm/session.h | 46 +++++++++++++ 9 files changed, 295 insertions(+) create mode 100644 lib/olm/message.cpp create mode 100644 lib/olm/message.h create mode 100644 lib/olm/session.cpp create mode 100644 lib/olm/session.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a359ae07..476b7d81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,6 +184,8 @@ list(APPEND lib_SRCS lib/olm/qolmoutboundsession.cpp lib/olm/utils.cpp lib/olm/errors.cpp + lib/olm/session.cpp + lib/olm/message.cpp ) # Configure API files generation diff --git a/lib/olm/message.cpp b/lib/olm/message.cpp new file mode 100644 index 00000000..0998a66b --- /dev/null +++ b/lib/olm/message.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "olm/message.h" + +using namespace Quotient; + +Message::Message(const QByteArray &ciphertext, Message::Type type) + : QByteArray(std::move(ciphertext)), _messageType(type) +{ + Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); +} + +Message::Message(QByteArray ciphertext) : QByteArray(std::move(ciphertext)) +{ + Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); +} + +Message::Type Message::type() const +{ + return _messageType; +} + +QByteArray Message::toCiphertext() const +{ + return QByteArray(*this); +} + + +#endif // Quotient_E2EE_ENABLED + + + diff --git a/lib/olm/message.h b/lib/olm/message.h new file mode 100644 index 00000000..6c8ab485 --- /dev/null +++ b/lib/olm/message.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED + +#include +#include + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class Message : private QByteArray { + Q_GADGET +public: + enum Type { + General, + PreKey, + }; + Q_ENUM(Type) + + Message() = default; + explicit Message(const QByteArray& ciphertext, Type type = General); + explicit Message(QByteArray ciphertext); + + static Message fromCiphertext(QByteArray ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type _messageType = General; +}; + + +} //namespace Quotient + +#endif // Quotient_E2EE_ENABLED diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index 8872f66e..9530d675 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -192,4 +192,9 @@ QByteArray QOlmAccount::signOneTimeKey(const QString &key) const return sign(j.toJson()); } +OlmAccount *Quotient::QOlmAccount::data() +{ + return m_account; +} + #endif diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h index 3b55212d..3260ca71 100644 --- a/lib/olm/qolmaccount.h +++ b/lib/olm/qolmaccount.h @@ -63,6 +63,10 @@ public: QByteArray signOneTimeKey(const QString &key) const; SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + OlmAccount *data(); + + // HACK do not use directly + QOlmAccount(OlmAccount *account); private: OlmAccount *m_account = nullptr; QString m_userId; diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h index eb698868..739a8411 100644 --- a/lib/olm/qolminboundsession.h +++ b/lib/olm/qolminboundsession.h @@ -46,5 +46,6 @@ private: }; using QOlmInboundGroupSessionPtr = std::unique_ptr; +using OlmInboundGroupSessionPtr = std::unique_ptr; } // namespace Quotient #endif diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h index a642f581..70c4d27f 100644 --- a/lib/olm/qolmoutboundsession.h +++ b/lib/olm/qolmoutboundsession.h @@ -49,5 +49,6 @@ private: }; using QOlmOutboundGroupSessionPtr = std::unique_ptr; +using OlmOutboundGroupSessionPtr = std::unique_ptr; } #endif diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp new file mode 100644 index 00000000..a2a7d28a --- /dev/null +++ b/lib/olm/session.cpp @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "olm/session.h" +#include "olm/utils.h" +#include "logging.h" + +using namespace Quotient; + +OlmError lastError(OlmSession* session) { + const std::string error_raw = olm_session_last_error(session); + + return fromString(error_raw); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); +} + +OlmSession* QOlmSession::create() +{ + return olm_session(new uint8_t[olm_session_size()]); +} + +std::unique_ptr QOlmSession::createInbound(QOlmAccount &account, const Message &preKeyMessage, bool from, const QString &theirIdentityKey) +{ + if (preKeyMessage.type() != Message::PreKey) { + qCDebug(E2EE) << "The message is not a pre-key"; + throw BadMessageFormat; + } + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account.data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account.data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + throw lastError(olmSession); + } + + return std::make_unique(olmSession); +} + +std::unique_ptr QOlmSession::createInboundSession(QOlmAccount& account, const Message &preKeyMessage) +{ + return createInbound(account, preKeyMessage); +} + +std::unique_ptr QOlmSession::createInboundSessionFrom(QOlmAccount &account, const QString &theirIdentityKey, const Message &preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +std::unique_ptr QOlmSession::createOutboundSession(QOlmAccount &account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +{ + auto *olmOutboundSession = create(); + const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); + QByteArray randomBuf = getRandom(randomLen); + + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); + const auto error = olm_create_outbound_session(olmOutboundSession, + account.data(), + theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + theirOneTimeKeyBuf.data(), theirOneTimeKeyBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + throw lastError(olmOutboundSession); + } + + randomBuf.clear(); + return std::make_unique(olmOutboundSession); +} + +std::variant QOlmSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + + key.clear(); + + return pickledBuf; +} + +std::variant, OlmError> QOlmSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmSession = create(); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), + pickled.data(), pickled.length()); + if (error == olm_error()) { + return lastError(olmSession); + } + + key.clear(); + return std::make_unique(olmSession); +} + +std::variant QOlmSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto randomLen = olm_encrypt_random_length(m_session); + QByteArray randomBuf = getRandom(randomLen); + const auto error = olm_encrypt(m_session, + reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length(), + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + + return Message::fromCiphertext(messageBuf); +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_session); + } + return idBuffer; +} + +QOlmSession::QOlmSession(OlmSession *session): m_session(session) +{ + +} + +#endif // Quotient_E2EE_ENABLED + + + diff --git a/lib/olm/session.h b/lib/olm/session.h new file mode 100644 index 00000000..76c1df29 --- /dev/null +++ b/lib/olm/session.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED + +#include "olm/e2ee.h" +#include "olm/message.h" +#include "olm/errors.h" +#include "olm/qolmaccount.h" + +namespace Quotient { + +//! Either an outbound or inbound session for secure communication. +class QOlmSession +{ +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static std::unique_ptr createInboundSession(QOlmAccount& account, const Message& preKeyMessage); + static std::unique_ptr createInboundSessionFrom(QOlmAccount& account, const QString& theirIdentityKey, const Message& preKeyMessage); + static std::unique_ptr createOutboundSession(QOlmAccount& account, const QString& theirIdentityKey, const QString& theirOneTimeKey); + //! Serialises an `QOlmSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. + static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + std::variant encrypt(const QString &plaintext); + // TODO: WiP + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static std::unique_ptr createInbound(QOlmAccount& account, const Message& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; + +} //namespace Quotient + +#endif // Quotient_E2EE_ENABLED -- cgit v1.2.3 From d0d8b267753792d0310dd964b0b688d6262e6eb4 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Tue, 26 Jan 2021 21:55:13 +0300 Subject: Add missing reinterpret_cast for session data --- lib/olm/session.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index a2a7d28a..e7a57677 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -70,8 +70,8 @@ std::unique_ptr QOlmSession::createOutboundSession(QOlmAccount &acc QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); const auto error = olm_create_outbound_session(olmOutboundSession, account.data(), - theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), - theirOneTimeKeyBuf.data(), theirOneTimeKeyBuf.length(), + reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), + reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), reinterpret_cast(randomBuf.data()), randomBuf.length()); if (error == olm_error()) { -- cgit v1.2.3 From 57a218086d3c687cd26580ee2a0d2135646411dc Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 00:50:27 +0100 Subject: Add hehlper functions --- lib/olm/qolmaccount.cpp | 15 +++++++++++++++ lib/olm/qolmaccount.h | 20 +++++++++++++++++++- lib/olm/session.cpp | 26 +++++++++++++++++--------- lib/olm/session.h | 10 ++++++---- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index 9530d675..9c47bc87 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -197,4 +197,19 @@ OlmAccount *Quotient::QOlmAccount::data() return m_account; } +std::variant, OlmError> QOlmAccount::createInboundSession(const Message &preKeyMessage) +{ + return QOlmSession::createInboundSession(this, preKeyMessage); +} + +std::variant, OlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage) +{ + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); +} + +std::variant, OlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +{ + return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); +} + #endif diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h index 3260ca71..df5e1be2 100644 --- a/lib/olm/qolmaccount.h +++ b/lib/olm/qolmaccount.h @@ -7,12 +7,15 @@ #include "olm/e2ee.h" #include "olm/errors.h" #include "olm/olm.h" +#include "olm/session.h" #include struct OlmAccount; namespace Quotient { +class QOlmSession; + //! An olm account manages all cryptographic keys used on a device. //! \code{.cpp} //! const auto olmAccount = new QOlmAccount(this); @@ -63,10 +66,25 @@ public: QByteArray signOneTimeKey(const QString &key) const; SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; - OlmAccount *data(); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param message An Olm pre-key message that was encrypted for this account. + std::variant, OlmError> createInboundSession(const Message &preKeyMessage); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param theirIdentityKey - The identity key of an Olm account that + //! encrypted this Olm message. + std::variant, OlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage); + + //! Creates an outbound session for sending messages to a specific + /// identity and one time key. + std::variant, OlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); // HACK do not use directly QOlmAccount(OlmAccount *account); + OlmAccount *data(); private: OlmAccount *m_account = nullptr; QString m_userId; diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index e7a57677..b5cd7b81 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -25,7 +25,7 @@ OlmSession* QOlmSession::create() return olm_session(new uint8_t[olm_session_size()]); } -std::unique_ptr QOlmSession::createInbound(QOlmAccount &account, const Message &preKeyMessage, bool from, const QString &theirIdentityKey) +std::variant, OlmError> QOlmSession::createInbound(QOlmAccount *account, const Message &preKeyMessage, bool from, const QString &theirIdentityKey) { if (preKeyMessage.type() != Message::PreKey) { qCDebug(E2EE) << "The message is not a pre-key"; @@ -38,29 +38,33 @@ std::unique_ptr QOlmSession::createInbound(QOlmAccount &account, co QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); size_t error = 0; if (from) { - error = olm_create_inbound_session_from(olmSession, account.data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); } else { - error = olm_create_inbound_session(olmSession, account.data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); } if (error == olm_error()) { - throw lastError(olmSession); + const auto lastErr = lastError(olmSession); + if (lastErr == OlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; } return std::make_unique(olmSession); } -std::unique_ptr QOlmSession::createInboundSession(QOlmAccount& account, const Message &preKeyMessage) +std::variant, OlmError> QOlmSession::createInboundSession(QOlmAccount *account, const Message &preKeyMessage) { return createInbound(account, preKeyMessage); } -std::unique_ptr QOlmSession::createInboundSessionFrom(QOlmAccount &account, const QString &theirIdentityKey, const Message &preKeyMessage) +std::variant, OlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage) { return createInbound(account, preKeyMessage, true, theirIdentityKey); } -std::unique_ptr QOlmSession::createOutboundSession(QOlmAccount &account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +std::variant, OlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) { auto *olmOutboundSession = create(); const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); @@ -69,13 +73,17 @@ std::unique_ptr QOlmSession::createOutboundSession(QOlmAccount &acc QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); const auto error = olm_create_outbound_session(olmOutboundSession, - account.data(), + account->data(), reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), reinterpret_cast(randomBuf.data()), randomBuf.length()); if (error == olm_error()) { - throw lastError(olmOutboundSession); + const auto lastErr = lastError(olmOutboundSession); + if (lastErr == OlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; } randomBuf.clear(); diff --git a/lib/olm/session.h b/lib/olm/session.h index 76c1df29..e3a52c88 100644 --- a/lib/olm/session.h +++ b/lib/olm/session.h @@ -13,15 +13,17 @@ namespace Quotient { +class QOlmAccount; + //! Either an outbound or inbound session for secure communication. class QOlmSession { public: ~QOlmSession(); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::unique_ptr createInboundSession(QOlmAccount& account, const Message& preKeyMessage); - static std::unique_ptr createInboundSessionFrom(QOlmAccount& account, const QString& theirIdentityKey, const Message& preKeyMessage); - static std::unique_ptr createOutboundSession(QOlmAccount& account, const QString& theirIdentityKey, const QString& theirOneTimeKey); + static std::variant, OlmError> createInboundSession(QOlmAccount *account, const Message &preKeyMessage); + static std::variant, OlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage); + static std::variant, OlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. @@ -37,7 +39,7 @@ public: private: //! Helper function for creating new sessions and handling errors. static OlmSession* create(); - static std::unique_ptr createInbound(QOlmAccount& account, const Message& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); + static std::variant, OlmError> createInbound(QOlmAccount *account, const Message& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); OlmSession* m_session; }; -- cgit v1.2.3 From fe2d5dd577a05e4a0e250d89487cd14025204b02 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 01:43:49 +0100 Subject: Start adding test for session stuff --- autotests/CMakeLists.txt | 1 + autotests/testolmsession.cpp | 28 ++++++++++++++++++++++++++++ lib/olm/message.cpp | 15 ++++++++------- lib/olm/message.h | 7 +++---- lib/olm/qolmaccount.cpp | 2 ++ lib/olm/session.cpp | 4 ++-- lib/olm/session.h | 4 ++++ 7 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 autotests/testolmsession.cpp diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 31cdb446..f35890a5 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -14,3 +14,4 @@ endfunction() quotient_add_test(NAME callcandidateseventtest) quotient_add_test(NAME testolmaccount) quotient_add_test(NAME testgroupsession) +quotient_add_test(NAME testolmsession) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp new file mode 100644 index 00000000..1b7fbb9b --- /dev/null +++ b/autotests/testolmsession.cpp @@ -0,0 +1,28 @@ +#include "olm/session.h" + +using namespace Quotient; + +std::pair, std::unique_ptr> createSessionPair() +{ + QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); + QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr"); + auto accountA = QOlmAccount("accountA:foo.com", "Device1UserA"); + accountA.unpickle(pickledAccountA, Unencrypted{}); + auto accountB = QOlmAccount("accountB:foo.com", "Device1UserB"); + accountB.unpickle(pickledAccountB, Unencrypted{}); + + const QByteArray identityKeyA("qIEr3TWcJQt4CP8QoKKJcCaukByIOpgh6erBkhLEa2o"); + const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk"); + const QByteArray identityKeyB("q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg"); + const QByteArray oneTimeKeyB("oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ"); + auto outbound = std::get>(accountA + .createOutboundSession(identityKeyB, oneTimeKeyB)); + + const auto preKey = std::get(outbound->encrypt("")); // Payload does not matter for PreKey + + if (preKey.type() != Message::General) { + throw "Wrong first message type received, can't create session"; + } + auto inbound = std::get>(accountB.createInboundSession(preKey)); + return std::make_pair, std::unique_ptr>(std::move(inbound), std::move(outbound)); +} diff --git a/lib/olm/message.cpp b/lib/olm/message.cpp index 0998a66b..634a6f0c 100644 --- a/lib/olm/message.cpp +++ b/lib/olm/message.cpp @@ -8,19 +8,15 @@ using namespace Quotient; Message::Message(const QByteArray &ciphertext, Message::Type type) - : QByteArray(std::move(ciphertext)), _messageType(type) -{ - Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); -} - -Message::Message(QByteArray ciphertext) : QByteArray(std::move(ciphertext)) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) { Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); } Message::Type Message::type() const { - return _messageType; + return m_messageType; } QByteArray Message::toCiphertext() const @@ -28,6 +24,11 @@ QByteArray Message::toCiphertext() const return QByteArray(*this); } +Message Message::fromCiphertext(const QByteArray &ciphertext) +{ + return Message(ciphertext, Message::General); +} + #endif // Quotient_E2EE_ENABLED diff --git a/lib/olm/message.h b/lib/olm/message.h index 6c8ab485..067d9b5a 100644 --- a/lib/olm/message.h +++ b/lib/olm/message.h @@ -28,16 +28,15 @@ public: Q_ENUM(Type) Message() = default; - explicit Message(const QByteArray& ciphertext, Type type = General); - explicit Message(QByteArray ciphertext); + explicit Message(const QByteArray &ciphertext, Type type = General); - static Message fromCiphertext(QByteArray ciphertext); + static Message fromCiphertext(const QByteArray &ciphertext); Q_INVOKABLE Type type() const; Q_INVOKABLE QByteArray toCiphertext() const; private: - Type _messageType = General; + Type m_messageType = General; }; diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp index 9c47bc87..ef51a395 100644 --- a/lib/olm/qolmaccount.cpp +++ b/lib/olm/qolmaccount.cpp @@ -199,11 +199,13 @@ OlmAccount *Quotient::QOlmAccount::data() std::variant, OlmError> QOlmAccount::createInboundSession(const Message &preKeyMessage) { + Q_ASSERT(preKeyMessage.type() == Message::PreKey); return QOlmSession::createInboundSession(this, preKeyMessage); } std::variant, OlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage) { + Q_ASSERT(preKeyMessage.type() == Message::PreKey); return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); } diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index b5cd7b81..f6cab650 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -152,9 +152,9 @@ QByteArray QOlmSession::sessionId() const return idBuffer; } -QOlmSession::QOlmSession(OlmSession *session): m_session(session) +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) { - } #endif // Quotient_E2EE_ENABLED diff --git a/lib/olm/session.h b/lib/olm/session.h index e3a52c88..89f5d822 100644 --- a/lib/olm/session.h +++ b/lib/olm/session.h @@ -14,6 +14,8 @@ namespace Quotient { class QOlmAccount; +class QOlmSession; + //! Either an outbound or inbound session for secure communication. class QOlmSession @@ -43,6 +45,8 @@ private: OlmSession* m_session; }; +//using QOlmSessionPtr = std::unique_ptr; + } //namespace Quotient #endif // Quotient_E2EE_ENABLED -- cgit v1.2.3 From f3fdd967d544650f9af8aadbaddfcf6d8a9fe957 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 02:08:09 +0100 Subject: Add first session test and it fails :( --- autotests/testolmsession.cpp | 19 ++++++++++++++++++- autotests/testolmsession.h | 14 ++++++++++++++ lib/olm/session.cpp | 24 +++++++++++++++++++++--- lib/olm/session.h | 7 ++++++- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 autotests/testolmsession.h diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 1b7fbb9b..6fa2a380 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -1,7 +1,13 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "olm/session.h" +#include "testolmsession.h" using namespace Quotient; +#ifdef Quotient_E2EE_ENABLED std::pair, std::unique_ptr> createSessionPair() { QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); @@ -18,7 +24,7 @@ std::pair, std::unique_ptr> createSess auto outbound = std::get>(accountA .createOutboundSession(identityKeyB, oneTimeKeyB)); - const auto preKey = std::get(outbound->encrypt("")); // Payload does not matter for PreKey + const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey if (preKey.type() != Message::General) { throw "Wrong first message type received, can't create session"; @@ -26,3 +32,14 @@ std::pair, std::unique_ptr> createSess auto inbound = std::get>(accountB.createInboundSession(preKey)); return std::make_pair, std::unique_ptr>(std::move(inbound), std::move(outbound)); } +#endif + +void TestOlmSession::olmOutboundSessionCreation() +{ +#ifdef Quotient_E2EE_ENABLED + const auto [_, outboundSession] = createSessionPair(); + QCOMPARE(0, outboundSession->hasReceivedMessage()); +#endif +} + +QTEST_MAIN(TestOlmSession) diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h new file mode 100644 index 00000000..7e3fc6e4 --- /dev/null +++ b/autotests/testolmsession.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include + +class TestOlmSession : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void olmOutboundSessionCreation(); +}; +#endif diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index f6cab650..0beb136e 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -121,11 +121,12 @@ std::variant, OlmError> QOlmSession::unpickle(QByte return std::make_unique(olmSession); } -std::variant QOlmSession::encrypt(const QString &plaintext) +Message QOlmSession::encrypt(const QString &plaintext) { QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); QByteArray messageBuf(messageMaxLen, '0'); + const auto messageType = encryptMessageType(); const auto randomLen = olm_encrypt_random_length(m_session); QByteArray randomBuf = getRandom(randomLen); const auto error = olm_encrypt(m_session, @@ -134,10 +135,22 @@ std::variant QOlmSession::encrypt(const QString &plaintext) reinterpret_cast(messageBuf.data()), messageBuf.length()); if (error == olm_error()) { - return lastError(m_session); + throw lastError(m_session); } - return Message::fromCiphertext(messageBuf); + return Message(messageBuf, messageType); +} + +Message::Type QOlmSession::encryptMessageType() +{ + const auto messageTypeResult = olm_encrypt_message_type(m_session); + if (messageTypeResult == olm_error()) { + throw lastError(m_session); + } + if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { + return Message::PreKey; + } + return Message::General; } QByteArray QOlmSession::sessionId() const @@ -152,6 +165,11 @@ QByteArray QOlmSession::sessionId() const return idBuffer; } +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + QOlmSession::QOlmSession(OlmSession *session) : m_session(session) { diff --git a/lib/olm/session.h b/lib/olm/session.h index 89f5d822..f9221dec 100644 --- a/lib/olm/session.h +++ b/lib/olm/session.h @@ -31,12 +31,17 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. - std::variant encrypt(const QString &plaintext); + Message encrypt(const QString &plaintext); // TODO: WiP //! Get a base64-encoded identifier for this session. QByteArray sessionId() const; + //! The type of the next message that will be returned from encryption. + Message::Type encryptMessageType(); + + bool hasReceivedMessage() const; + QOlmSession(OlmSession* session); private: //! Helper function for creating new sessions and handling errors. -- cgit v1.2.3 From 069602584e0f3ec10a26380af69b95f5da11a8b7 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 02:26:23 +0100 Subject: Add more test and methods in session handling --- autotests/testolmsession.cpp | 18 +++++++++++++++++- autotests/testolmsession.h | 1 + lib/olm/message.cpp | 6 ++++++ lib/olm/message.h | 3 ++- lib/olm/qolmoutboundsession.cpp | 2 +- lib/olm/session.cpp | 20 ++++++++++++++++++++ lib/olm/session.h | 4 ++++ 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 6fa2a380..2f7a82e9 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -26,7 +26,7 @@ std::pair, std::unique_ptr> createSess const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey - if (preKey.type() != Message::General) { + if (preKey.type() != Message::PreKey) { throw "Wrong first message type received, can't create session"; } auto inbound = std::get>(accountB.createInboundSession(preKey)); @@ -42,4 +42,20 @@ void TestOlmSession::olmOutboundSessionCreation() #endif } +void TestOlmSession::olmEncryptDecrypt() +{ +#ifdef Quotient_E2EE_ENABLED + const auto [inboundSession, outboundSession] = createSessionPair(); + const auto encrypted = outboundSession->encrypt("Hello world!"); + if (encrypted.type() == Message::PreKey) { + Message m(encrypted); // clone + QVERIFY(std::get(inboundSession->matchesInboundSession(m))); + } + + //const auto decrypted = inboundSession->decrypt(encrypted); + + //QCOMPARE(decrypted, "Hello world!"); +#endif +} + QTEST_MAIN(TestOlmSession) diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h index 7e3fc6e4..49e8c3e3 100644 --- a/autotests/testolmsession.h +++ b/autotests/testolmsession.h @@ -10,5 +10,6 @@ class TestOlmSession : public QObject Q_OBJECT private Q_SLOTS: void olmOutboundSessionCreation(); + void olmEncryptDecrypt(); }; #endif diff --git a/lib/olm/message.cpp b/lib/olm/message.cpp index 634a6f0c..ac7038ae 100644 --- a/lib/olm/message.cpp +++ b/lib/olm/message.cpp @@ -14,6 +14,12 @@ Message::Message(const QByteArray &ciphertext, Message::Type type) Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); } +Message::Message(const Message &message) + : QByteArray(message) + , m_messageType(message.type()) +{ +} + Message::Type Message::type() const { return m_messageType; diff --git a/lib/olm/message.h b/lib/olm/message.h index 067d9b5a..d2fe871e 100644 --- a/lib/olm/message.h +++ b/lib/olm/message.h @@ -18,7 +18,7 @@ namespace Quotient { * * The class provides functions to get a type and the ciphertext. */ -class Message : private QByteArray { +class Message : public QByteArray { Q_GADGET public: enum Type { @@ -29,6 +29,7 @@ public: Message() = default; explicit Message(const QByteArray &ciphertext, Type type = General); + explicit Message(const Message &message); static Message fromCiphertext(const QByteArray &ciphertext); diff --git a/lib/olm/qolmoutboundsession.cpp b/lib/olm/qolmoutboundsession.cpp index 4f3cc827..e5c43495 100644 --- a/lib/olm/qolmoutboundsession.cpp +++ b/lib/olm/qolmoutboundsession.cpp @@ -22,7 +22,7 @@ QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *sess QOlmOutboundGroupSession::~QOlmOutboundGroupSession() { olm_clear_outbound_group_session(m_groupSession); - //delete[](reinterpret_cast(m_groupSession)); + delete[](reinterpret_cast(m_groupSession)); } std::unique_ptr QOlmOutboundGroupSession::create() diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index 0beb136e..d0493fe8 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -18,6 +18,7 @@ OlmError lastError(OlmSession* session) { Quotient::QOlmSession::~QOlmSession() { olm_clear_session(m_session); + delete[](reinterpret_cast(m_session)); } OlmSession* QOlmSession::create() @@ -170,6 +171,25 @@ bool QOlmSession::hasReceivedMessage() const return olm_session_has_received_message(m_session); } +std::variant QOlmSession::matchesInboundSession(Message &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == Message::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + + if (matchesResult == olm_error()) { + return lastError(m_session); + } + switch (matchesResult) { + case 0: + return false; + case 1: + return true; + default: + return OlmError::Unknown; + } +} + QOlmSession::QOlmSession(OlmSession *session) : m_session(session) { diff --git a/lib/olm/session.h b/lib/olm/session.h index f9221dec..c45b6898 100644 --- a/lib/olm/session.h +++ b/lib/olm/session.h @@ -40,8 +40,12 @@ public: //! The type of the next message that will be returned from encryption. Message::Type encryptMessageType(); + //! Checker for any received messages for this session. bool hasReceivedMessage() const; + //! Checks if the 'prekey' message is for this in-bound session. + std::variant matchesInboundSession(Message &preKeyMessage); + QOlmSession(OlmSession* session); private: //! Helper function for creating new sessions and handling errors. -- cgit v1.2.3 From eabea7af10b5734a507484478a64d2c9f716279f Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 15:33:46 +0100 Subject: Add QOlmSession::decrypt --- autotests/testolmsession.cpp | 4 ++-- lib/olm/session.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ lib/olm/session.h | 6 +++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 2f7a82e9..fc151621 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -52,9 +52,9 @@ void TestOlmSession::olmEncryptDecrypt() QVERIFY(std::get(inboundSession->matchesInboundSession(m))); } - //const auto decrypted = inboundSession->decrypt(encrypted); + const auto decrypted = std::get(inboundSession->decrypt(encrypted)); - //QCOMPARE(decrypted, "Hello world!"); + QCOMPARE(decrypted, "Hello world!"); #endif } diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index d0493fe8..a05e0786 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -6,6 +6,7 @@ #include "olm/session.h" #include "olm/utils.h" #include "logging.h" +#include using namespace Quotient; @@ -142,6 +143,46 @@ Message QOlmSession::encrypt(const QString &plaintext) return Message(messageBuf, messageType); } +std::variant QOlmSession::decrypt(const Message &message) const +{ + const auto messageType = message.type(); + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = messageType == Message::Type::General + ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (plaintextMaxLen == olm_error()) { + return lastError(m_session); + } + + QByteArray plaintextBuf(plaintextMaxLen, '0'); + QByteArray messageBuf2(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, + reinterpret_cast(messageBuf2.data()), messageBuf2.length(), + reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); + + if (plaintextResultLen == olm_error()) { + const auto lastErr = lastError(m_session); + if (lastErr == OlmError::OutputBufferTooSmall) { + throw lastErr; + } + return lastErr; + } + QByteArray output(plaintextResultLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); + plaintextBuf.clear(); + return output; +} + Message::Type QOlmSession::encryptMessageType() { const auto messageTypeResult = olm_encrypt_message_type(m_session); diff --git a/lib/olm/session.h b/lib/olm/session.h index c45b6898..3f1622c7 100644 --- a/lib/olm/session.h +++ b/lib/olm/session.h @@ -32,7 +32,11 @@ public: static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. Message encrypt(const QString &plaintext); - // TODO: WiP + + //! Decrypts a message using this session. Decoding is lossy, meaing if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + std::variant decrypt(const Message &message) const; //! Get a base64-encoded identifier for this session. QByteArray sessionId() const; -- cgit v1.2.3 From 583d484b2dc27d3216706a1e0858b794d4c5fe19 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 16:09:10 +0100 Subject: Implement session sorting --- autotests/testolmsession.cpp | 26 ++++++++++++++++++++++++++ autotests/testolmsession.h | 1 + lib/olm/session.cpp | 4 ++-- lib/olm/session.h | 13 ++++++++++++- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index fc151621..77ba35ef 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -58,4 +58,30 @@ void TestOlmSession::olmEncryptDecrypt() #endif } +void TestOlmSession::correctSessionOrdering() +{ +#ifdef Quotient_E2EE_ENABLED + // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM + auto session1 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{})); + // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0 + auto session2 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {})); + // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME + auto session3 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{})); + + const auto session1Id = session1->sessionId(); + const auto session2Id = session2->sessionId(); + const auto session3Id = session3->sessionId(); + + std::vector> sessionList; + sessionList.push_back(std::move(session1)); + sessionList.push_back(std::move(session2)); + sessionList.push_back(std::move(session3)); + + std::sort(sessionList.begin(), sessionList.end()); + QCOMPARE(sessionList[0]->sessionId(), session2Id); + QCOMPARE(sessionList[1]->sessionId(), session3Id); + QCOMPARE(sessionList[2]->sessionId(), session1Id); +#endif +} + QTEST_MAIN(TestOlmSession) diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h index 49e8c3e3..c03b7b6a 100644 --- a/autotests/testolmsession.h +++ b/autotests/testolmsession.h @@ -11,5 +11,6 @@ class TestOlmSession : public QObject private Q_SLOTS: void olmOutboundSessionCreation(); void olmEncryptDecrypt(); + void correctSessionOrdering(); }; #endif diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp index a05e0786..94f12db6 100644 --- a/lib/olm/session.cpp +++ b/lib/olm/session.cpp @@ -108,13 +108,13 @@ std::variant QOlmSession::pickle(const PicklingMode &mode) return pickledBuf; } -std::variant, OlmError> QOlmSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +std::variant, OlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmSession = create(); QByteArray key = toKey(mode); const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), - pickled.data(), pickled.length()); + pickledBuf.data(), pickledBuf.length()); if (error == olm_error()) { return lastError(olmSession); } diff --git a/lib/olm/session.h b/lib/olm/session.h index 3f1622c7..03b3514e 100644 --- a/lib/olm/session.h +++ b/lib/olm/session.h @@ -9,6 +9,7 @@ #include "olm/e2ee.h" #include "olm/message.h" #include "olm/errors.h" +#include #include "olm/qolmaccount.h" namespace Quotient { @@ -29,7 +30,7 @@ public: //! Serialises an `QOlmSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + static std::variant, OlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. Message encrypt(const QString &plaintext); @@ -50,6 +51,15 @@ public: //! Checks if the 'prekey' message is for this in-bound session. std::variant matchesInboundSession(Message &preKeyMessage); + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { + return *lhs < *rhs; + } + QOlmSession(OlmSession* session); private: //! Helper function for creating new sessions and handling errors. @@ -58,6 +68,7 @@ private: OlmSession* m_session; }; + //using QOlmSessionPtr = std::unique_ptr; } //namespace Quotient -- cgit v1.2.3 From efe7e4ebc9c71f68d29c5c1a5a6bacbaea6fd146 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 16:12:17 +0100 Subject: Disable olm test when disabling encryption --- autotests/CMakeLists.txt | 8 +++++--- autotests/testgroupsession.cpp | 2 -- autotests/testgroupsession.h | 2 -- autotests/testolmaccount.cpp | 2 -- autotests/testolmaccount.h | 2 -- autotests/testolmsession.cpp | 8 -------- autotests/testolmsession.h | 2 -- 7 files changed, 5 insertions(+), 21 deletions(-) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index f35890a5..6afdf8cc 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -12,6 +12,8 @@ function(QUOTIENT_ADD_TEST) endfunction() quotient_add_test(NAME callcandidateseventtest) -quotient_add_test(NAME testolmaccount) -quotient_add_test(NAME testgroupsession) -quotient_add_test(NAME testolmsession) +if(${PROJECT_NAME}_ENABLE_E2EE) + quotient_add_test(NAME testolmaccount) + quotient_add_test(NAME testgroupsession) + quotient_add_test(NAME testolmsession) +endif() diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index a99172d7..23c5bf8f 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "testgroupsession.h" #include "olm/qolminboundsession.h" #include "olm/qolmoutboundsession.h" @@ -54,4 +53,3 @@ void TestOlmSession::groupSessionCryptoValid() QCOMPARE(0, decryptionResult.second); } QTEST_MAIN(TestOlmSession) -#endif diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h index c9192990..27f34bec 100644 --- a/autotests/testgroupsession.h +++ b/autotests/testgroupsession.h @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include class TestOlmSession : public QObject @@ -13,4 +12,3 @@ private Q_SLOTS: void groupSessionPicklingValid(); void groupSessionCryptoValid(); }; -#endif diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 75102c32..9f85e77e 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "testolmaccount.h" #include "olm/qolmaccount.h" @@ -69,4 +68,3 @@ void TestOlmAccount::oneTimeKeysValid() } QTEST_MAIN(TestOlmAccount) -#endif diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index c3297b5f..e7b32b8b 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include class TestOlmAccount : public QObject @@ -16,4 +15,3 @@ private Q_SLOTS: void oneTimeKeysValid(); //void removeOneTimeKeys(); }; -#endif diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 77ba35ef..da0e36e3 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -7,7 +7,6 @@ using namespace Quotient; -#ifdef Quotient_E2EE_ENABLED std::pair, std::unique_ptr> createSessionPair() { QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); @@ -32,19 +31,15 @@ std::pair, std::unique_ptr> createSess auto inbound = std::get>(accountB.createInboundSession(preKey)); return std::make_pair, std::unique_ptr>(std::move(inbound), std::move(outbound)); } -#endif void TestOlmSession::olmOutboundSessionCreation() { -#ifdef Quotient_E2EE_ENABLED const auto [_, outboundSession] = createSessionPair(); QCOMPARE(0, outboundSession->hasReceivedMessage()); -#endif } void TestOlmSession::olmEncryptDecrypt() { -#ifdef Quotient_E2EE_ENABLED const auto [inboundSession, outboundSession] = createSessionPair(); const auto encrypted = outboundSession->encrypt("Hello world!"); if (encrypted.type() == Message::PreKey) { @@ -55,12 +50,10 @@ void TestOlmSession::olmEncryptDecrypt() const auto decrypted = std::get(inboundSession->decrypt(encrypted)); QCOMPARE(decrypted, "Hello world!"); -#endif } void TestOlmSession::correctSessionOrdering() { -#ifdef Quotient_E2EE_ENABLED // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM auto session1 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{})); // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0 @@ -81,7 +74,6 @@ void TestOlmSession::correctSessionOrdering() QCOMPARE(sessionList[0]->sessionId(), session2Id); QCOMPARE(sessionList[1]->sessionId(), session3Id); QCOMPARE(sessionList[2]->sessionId(), session1Id); -#endif } QTEST_MAIN(TestOlmSession) diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h index c03b7b6a..9a5798fa 100644 --- a/autotests/testolmsession.h +++ b/autotests/testolmsession.h @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include class TestOlmSession : public QObject @@ -13,4 +12,3 @@ private Q_SLOTS: void olmEncryptDecrypt(); void correctSessionOrdering(); }; -#endif -- cgit v1.2.3 From d44a7914c5f2ba231fdd9c830b1eace69c3383d0 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 16:29:32 +0100 Subject: Add device key test --- autotests/testolmaccount.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++++ autotests/testolmaccount.h | 1 + 2 files changed, 66 insertions(+) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 9f85e77e..9a8e253c 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -4,6 +4,7 @@ #include "testolmaccount.h" #include "olm/qolmaccount.h" +#include "csapi/definitions/device_keys.h" using namespace Quotient; @@ -67,4 +68,68 @@ void TestOlmAccount::oneTimeKeysValid() QCOMPARE(20, oneTimeKeysFilled.curve25519().count()); } +void TestOlmAccount::deviceKeys() +{ + // copied from mtxclient + DeviceKeys device1; + device1.userId = "@alice:example.com"; + device1.deviceId = "JLAFKJWSCS"; + device1.keys = {{"curve25519:JLAFKJWSCS", "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"}, + {"ed25519:JLAFKJWSCS", "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"}}; + + // TODO that should be the default value + device1.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2"}; + + device1.signatures = { + {"@alice:example.com", + {{"ed25519:JLAFKJWSCS", + "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/" + "a+myXS367WT6NAIcBA"}}}}; + + QJsonObject j; + JsonObjectConverter::dumpTo(j, device1); + QJsonDocument doc(j); + QCOMPARE(doc.toJson(QJsonDocument::Compact), "{\"algorithms\":[\"m.olm.v1.curve25519-aes-sha2\",\"m.megolm.v1.aes-sha2\"]," + "\"device_id\":\"JLAFKJWSCS\",\"keys\":{\"curve25519:JLAFKJWSCS\":" + "\"3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI\",\"ed25519:JLAFKJWSCS\":" + "\"lEuiRJBit0IG6nUf5pUzWTUEsRVVe/" + "HJkoKuEww9ULI\"},\"signatures\":{\"@alice:example.com\":{\"ed25519:JLAFKJWSCS\":" + "\"dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/" + "a+myXS367WT6NAIcBA\"}},\"user_id\":\"@alice:example.com\"}"); + + auto doc2 = QJsonDocument::fromJson(R"({ + "user_id": "@alice:example.com", + "device_id": "JLAFKJWSCS", + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "keys": { + "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI", + "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI" + }, + "signatures": { + "@alice:example.com": { + "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" + } + }, + "unsigned": { + "device_display_name": "Alice's mobile phone" + } + })"); + + DeviceKeys device2; + JsonObjectConverter::fillFrom(doc2.object(), device2); + + QCOMPARE(device2.userId, device1.userId); + QCOMPARE(device2.deviceId, device1.deviceId); + QCOMPARE(device2.keys, device1.keys); + QCOMPARE(device2.algorithms, device1.algorithms); + QCOMPARE(device2.signatures, device1.signatures); + + // UnsignedDeviceInfo is missing from the generated DeviceKeys object :( + // QCOMPARE(device2.unsignedInfo.deviceDisplayName, "Alice's mobile phone"); +} + QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index e7b32b8b..547c25c1 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -14,4 +14,5 @@ private Q_SLOTS: void signatureValid(); void oneTimeKeysValid(); //void removeOneTimeKeys(); + void deviceKeys(); }; -- cgit v1.2.3 From 7ad805492f8b42a4bc854313695a912c89019957 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 17:54:27 +0100 Subject: Fix CI --- .github/workflows/ci.yml | 6 ++++++ CMakeLists.txt | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47e55421..0b707236 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,12 @@ jobs: with: arch: ${{ matrix.platform }} + - name: Install OpenSSL + if: contains(matrix.os, 'ubuntu') and matrix.e2ee + run: | + sudo apt-get install libssl-dev + echo "openssl version" >>$GITHUB_ENV + - name: Build and install olm if: matrix.e2ee run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 476b7d81..a04da04d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,14 +87,14 @@ find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModul get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") -find_package(OpenSSL 1.1.0 REQUIRED) -set_package_properties(OpenSSL PROPERTIES - DESCRIPTION "Open source SSL and TLS implementation and cryptographic library" - URL "https://www.openssl.org/" - TYPE REQUIRED -) - if (${PROJECT_NAME}_ENABLE_E2EE) + find_package(OpenSSL 1.1.0 REQUIRED) + set_package_properties(OpenSSL PROPERTIES + DESCRIPTION "Open source SSL and TLS implementation and cryptographic library" + URL "https://www.openssl.org/" + TYPE REQUIRED + ) + if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) add_subdirectory(3rdparty/libQtOlm) -- cgit v1.2.3 From 5910689306149cacf3ac644d3ea42e10f889e3fe Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 27 Jan 2021 20:16:59 +0100 Subject: Add encrypted file struct --- autotests/testolmaccount.cpp | 30 ++++++++++++++++++++++++++++++ autotests/testolmaccount.h | 1 + 2 files changed, 31 insertions(+) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 9a8e253c..2fac53bd 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -5,6 +5,7 @@ #include "testolmaccount.h" #include "olm/qolmaccount.h" #include "csapi/definitions/device_keys.h" +#include "events/encryptedfile.h" using namespace Quotient; @@ -132,4 +133,33 @@ void TestOlmAccount::deviceKeys() // QCOMPARE(device2.unsignedInfo.deviceDisplayName, "Alice's mobile phone"); } +void TestOlmAccount::encryptedFile() +{ + auto doc = QJsonDocument::fromJson(R"({ + "url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", + "v": "v2", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0", + "key_ops": ["encrypt","decrypt"], + "kty": "oct" + }, + "iv": "w+sE15fzSc0AAAAAAAAAAA", + "hashes": { + "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" + }})"); + + EncryptedFile file; + JsonObjectConverter::fillFrom(doc.object(), file); + + QCOMPARE(file.v, "v2"); + QCOMPARE(file.iv, "w+sE15fzSc0AAAAAAAAAAA"); + QCOMPARE(file.hashes["sha256"], "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA"); + QCOMPARE(file.key.alg, "A256CTR"); + QCOMPARE(file.key.ext, true); + QCOMPARE(file.key.k, "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0"); + QCOMPARE(file.key.keyOps.count(), 2); + QCOMPARE(file.key.kty, "oct"); +} QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 547c25c1..4e270730 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -15,4 +15,5 @@ private Q_SLOTS: void oneTimeKeysValid(); //void removeOneTimeKeys(); void deviceKeys(); + void encryptedFile(); }; -- cgit v1.2.3 From dd0316ce57bd9256a093d66845e1d40cd9426ba4 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 28 Jan 2021 21:54:37 +0100 Subject: Move files --- CMakeLists.txt | 16 +-- autotests/testgroupsession.cpp | 6 +- autotests/testolmaccount.cpp | 6 +- autotests/testolmsession.cpp | 2 +- lib/crypto/e2ee.h | 82 +++++++++++++ lib/crypto/errors.cpp | 21 ++++ lib/crypto/errors.h | 31 +++++ lib/crypto/message.cpp | 42 +++++++ lib/crypto/message.h | 46 +++++++ lib/crypto/qolmaccount.cpp | 217 +++++++++++++++++++++++++++++++++ lib/crypto/qolmaccount.h | 96 +++++++++++++++ lib/crypto/qolminboundsession.cpp | 157 ++++++++++++++++++++++++ lib/crypto/qolminboundsession.h | 51 ++++++++ lib/crypto/qolmoutboundsession.cpp | 131 ++++++++++++++++++++ lib/crypto/qolmoutboundsession.h | 54 +++++++++ lib/crypto/qolmsession.cpp | 29 +++++ lib/crypto/qolmsession.h | 49 ++++++++ lib/crypto/session.cpp | 242 +++++++++++++++++++++++++++++++++++++ lib/crypto/session.h | 77 ++++++++++++ lib/crypto/utils.cpp | 26 ++++ lib/crypto/utils.h | 15 +++ lib/olm/e2ee.h | 82 ------------- lib/olm/errors.cpp | 21 ---- lib/olm/errors.h | 31 ----- lib/olm/message.cpp | 42 ------- lib/olm/message.h | 46 ------- lib/olm/qolmaccount.cpp | 217 --------------------------------- lib/olm/qolmaccount.h | 96 --------------- lib/olm/qolminboundsession.cpp | 157 ------------------------ lib/olm/qolminboundsession.h | 51 -------- lib/olm/qolmoutboundsession.cpp | 131 -------------------- lib/olm/qolmoutboundsession.h | 54 --------- lib/olm/qolmsession.cpp | 29 ----- lib/olm/qolmsession.h | 49 -------- lib/olm/session.cpp | 242 ------------------------------------- lib/olm/session.h | 76 ------------ lib/olm/utils.cpp | 26 ---- lib/olm/utils.h | 15 --- 38 files changed, 1381 insertions(+), 1380 deletions(-) create mode 100644 lib/crypto/e2ee.h create mode 100644 lib/crypto/errors.cpp create mode 100644 lib/crypto/errors.h create mode 100644 lib/crypto/message.cpp create mode 100644 lib/crypto/message.h create mode 100644 lib/crypto/qolmaccount.cpp create mode 100644 lib/crypto/qolmaccount.h create mode 100644 lib/crypto/qolminboundsession.cpp create mode 100644 lib/crypto/qolminboundsession.h create mode 100644 lib/crypto/qolmoutboundsession.cpp create mode 100644 lib/crypto/qolmoutboundsession.h create mode 100644 lib/crypto/qolmsession.cpp create mode 100644 lib/crypto/qolmsession.h create mode 100644 lib/crypto/session.cpp create mode 100644 lib/crypto/session.h create mode 100644 lib/crypto/utils.cpp create mode 100644 lib/crypto/utils.h delete mode 100644 lib/olm/e2ee.h delete mode 100644 lib/olm/errors.cpp delete mode 100644 lib/olm/errors.h delete mode 100644 lib/olm/message.cpp delete mode 100644 lib/olm/message.h delete mode 100644 lib/olm/qolmaccount.cpp delete mode 100644 lib/olm/qolmaccount.h delete mode 100644 lib/olm/qolminboundsession.cpp delete mode 100644 lib/olm/qolminboundsession.h delete mode 100644 lib/olm/qolmoutboundsession.cpp delete mode 100644 lib/olm/qolmoutboundsession.h delete mode 100644 lib/olm/qolmsession.cpp delete mode 100644 lib/olm/qolmsession.h delete mode 100644 lib/olm/session.cpp delete mode 100644 lib/olm/session.h delete mode 100644 lib/olm/utils.cpp delete mode 100644 lib/olm/utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a04da04d..40767573 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,14 +178,14 @@ list(APPEND lib_SRCS lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp - lib/olm/qolmaccount.cpp - lib/olm/qolmsession.cpp - lib/olm/qolminboundsession.cpp - lib/olm/qolmoutboundsession.cpp - lib/olm/utils.cpp - lib/olm/errors.cpp - lib/olm/session.cpp - lib/olm/message.cpp + lib/crypto/qolmaccount.cpp + lib/crypto/qolmsession.cpp + lib/crypto/qolminboundsession.cpp + lib/crypto/qolmoutboundsession.cpp + lib/crypto/utils.cpp + lib/crypto/errors.cpp + lib/crypto/session.cpp + lib/crypto/message.cpp ) # Configure API files generation diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 23c5bf8f..325ca2ec 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -3,9 +3,9 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testgroupsession.h" -#include "olm/qolminboundsession.h" -#include "olm/qolmoutboundsession.h" -#include "olm/utils.h" +#include +#include +#include using namespace Quotient; diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 2fac53bd..cbce845a 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -3,9 +3,9 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testolmaccount.h" -#include "olm/qolmaccount.h" -#include "csapi/definitions/device_keys.h" -#include "events/encryptedfile.h" +#include +#include +#include using namespace Quotient; diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index da0e36e3..462c8213 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "olm/session.h" +#include #include "testolmsession.h" using namespace Quotient; diff --git a/lib/crypto/e2ee.h b/lib/crypto/e2ee.h new file mode 100644 index 00000000..74f876e4 --- /dev/null +++ b/lib/crypto/e2ee.h @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "util.h" +#include +#include +#include +#include + +#include + +namespace Quotient { +inline const auto CiphertextKeyL = "ciphertext"_ls; +inline const auto SenderKeyKeyL = "sender_key"_ls; +inline const auto DeviceIdKeyL = "device_id"_ls; +inline const auto SessionIdKeyL = "session_id"_ls; + +inline const auto AlgorithmKeyL = "algorithm"_ls; +inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +inline const auto AlgorithmKey = QStringLiteral("algorithm"); +inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); +inline const auto RotationPeriodMsgsKey = + QStringLiteral("rotation_period_msgs"); + +inline const auto Ed25519Key = QStringLiteral("ed25519"); +inline const auto Curve25519Key = QStringLiteral("curve25519"); +inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); +inline const auto OlmV1Curve25519AesSha2AlgoKey = + QStringLiteral("m.olm.v1.curve25519-aes-sha2"); +inline const auto MegolmV1AesSha2AlgoKey = + QStringLiteral("m.megolm.v1.aes-sha2"); +inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; + +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant; + +template struct overloaded : Ts... { using Ts::operator()...; }; +template overloaded(Ts...) -> overloaded; + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the one-time keys. +struct OneTimeKeys +{ + QMap> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QMap curve25519() const; + + //! Get a reference to the hashmap corresponding to given key type. + std::optional> get(QString keyType) const; +}; + +//! Struct representing the signed one-time keys. +struct SignedOneTimeKey +{ + //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. + QString key; + + //! Required. Signatures of the key object. + //! The signature is calculated using the process described at Signing JSON. + QMap> signatures; +}; + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); + +} // namespace Quotient diff --git a/lib/crypto/errors.cpp b/lib/crypto/errors.cpp new file mode 100644 index 00000000..00ff962d --- /dev/null +++ b/lib/crypto/errors.cpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later +#ifdef Quotient_E2EE_ENABLED +#include "crypto/errors.h" + +Quotient::OlmError Quotient::fromString(const std::string &error_raw) { + if (error_raw.compare("BAD_ACCOUNT_KEY")) { + return OlmError::BadAccountKey; + } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { + return OlmError::BadMessageKeyId; + } else if (error_raw.compare("INVALID_BASE64")) { + return OlmError::InvalidBase64; + } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { + return OlmError::NotEnoughRandom; + } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + return OlmError::OutputBufferTooSmall; + } else { + return OlmError::Unknown; + } +} +#endif diff --git a/lib/crypto/errors.h b/lib/crypto/errors.h new file mode 100644 index 00000000..09d2a989 --- /dev/null +++ b/lib/crypto/errors.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED +#include + +namespace Quotient { +//! All errors that could be caused by an operation regarding Olm +//! Errors are named exactly like the ones in libolm. +enum OlmError +{ + BadAccountKey, + BadMessageFormat, + BadMessageKeyId, + BadMessageMac, + BadMessageVersion, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + UnknownMessageIndex, + Unknown, +}; + +OlmError fromString(const std::string &error_raw); + +} //namespace Quotient + +#endif diff --git a/lib/crypto/message.cpp b/lib/crypto/message.cpp new file mode 100644 index 00000000..830633bf --- /dev/null +++ b/lib/crypto/message.cpp @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/message.h" + +using namespace Quotient; + +Message::Message(const QByteArray &ciphertext, Message::Type type) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) +{ + Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); +} + +Message::Message(const Message &message) + : QByteArray(message) + , m_messageType(message.type()) +{ +} + +Message::Type Message::type() const +{ + return m_messageType; +} + +QByteArray Message::toCiphertext() const +{ + return QByteArray(*this); +} + +Message Message::fromCiphertext(const QByteArray &ciphertext) +{ + return Message(ciphertext, Message::General); +} + + +#endif // Quotient_E2EE_ENABLED + + + diff --git a/lib/crypto/message.h b/lib/crypto/message.h new file mode 100644 index 00000000..1ae19ba8 --- /dev/null +++ b/lib/crypto/message.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED + +#include +#include + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class Message : public QByteArray { + Q_GADGET +public: + enum Type { + General, + PreKey, + }; + Q_ENUM(Type) + + Message() = default; + explicit Message(const QByteArray &ciphertext, Type type = General); + explicit Message(const Message &message); + + static Message fromCiphertext(const QByteArray &ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type m_messageType = General; +}; + + +} //namespace Quotient + +#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp new file mode 100644 index 00000000..8824e7ef --- /dev/null +++ b/lib/crypto/qolmaccount.cpp @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/qolmaccount.h" +#include "crypto/utils.h" +#include +#include +#include +#include + +using namespace Quotient; + +QMap OneTimeKeys::curve25519() const +{ + return keys[QStringLiteral("curve25519")]; +} + +std::optional> OneTimeKeys::get(QString keyType) const +{ + if (!keys.contains(keyType)) { + return std::nullopt; + } + return keys[keyType]; +} + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) +{ + return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; +} + +// Convert olm error to enum +OlmError lastError(OlmAccount *account) { + const std::string error_raw = olm_account_last_error(account); + + return fromString(error_raw); +} + +QByteArray getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + std::generate(buffer.begin(), buffer.end(), std::rand); + return buffer; +} + +QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId) + : m_userId(userId) + , m_deviceId(deviceId) +{ +} + +QOlmAccount::~QOlmAccount() +{ + olm_clear_account(m_account); + delete[](reinterpret_cast(m_account)); +} + +void QOlmAccount::createNewAccount() +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + size_t randomSize = olm_create_account_random_length(m_account); + QByteArray randomData = getRandom(randomSize); + const auto error = olm_create_account(m_account, randomData.data(), randomSize); + if (error == olm_error()) { + throw lastError(m_account); + } +} + +void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + const QByteArray key = toKey(mode); + const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); + if (error == olm_error()) { + throw lastError(m_account); + } +} + +std::variant QOlmAccount::pickle(const PicklingMode &mode) +{ + const QByteArray key = toKey(mode); + const size_t pickleLength = olm_pickle_account_length(m_account); + QByteArray pickleBuffer(pickleLength, '0'); + const auto error = olm_pickle_account(m_account, key.data(), + key.length(), pickleBuffer.data(), pickleLength); + if (error == olm_error()) { + return lastError(m_account); + } + return pickleBuffer; +} + +IdentityKeys QOlmAccount::identityKeys() const +{ + const size_t keyLength = olm_account_identity_keys_length(m_account); + QByteArray keyBuffer(keyLength, '0'); + const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); + if (error == olm_error()) { + throw lastError(m_account); + } + const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); + return IdentityKeys { + key.value(QStringLiteral("curve25519")).toString().toUtf8(), + key.value(QStringLiteral("ed25519")).toString().toUtf8() + }; +} + +QByteArray QOlmAccount::sign(const QByteArray &message) const +{ + const size_t signatureLength = olm_account_signature_length(m_account); + QByteArray signatureBuffer(signatureLength, '0'); + const auto error = olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureLength); + + if (error == olm_error()) { + throw lastError(m_account); + } + return signatureBuffer; +} + +QByteArray QOlmAccount::signIdentityKeys() const +{ + const auto keys = identityKeys(); + const QJsonObject j{ {Curve25519Key, QString(keys.curve25519)}, {Ed25519Key, QString(keys.ed25519)} }; + QJsonDocument doc; + doc.setObject(j); + return sign(doc.toJson()); + +} + +size_t QOlmAccount::maxNumberOfOneTimeKeys() const +{ + return olm_account_max_number_of_one_time_keys(m_account); +} + +void QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +{ + const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + QByteArray randomBuffer = getRandom(randomLen); + const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLen); + + if (error == olm_error()) { + throw lastError(m_account); + } +} + +OneTimeKeys QOlmAccount::oneTimeKeys() const +{ + const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); + + const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); + if (error == olm_error()) { + throw lastError(m_account); + } + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); + OneTimeKeys oneTimeKeys; + + for (const QJsonValue &key1 : json.keys()) { + auto oneTimeKeyObject = json[key1.toString()].toObject(); + auto keyMap = QMap(); + for (const QString &key2 : oneTimeKeyObject.keys()) { + keyMap[key2] = oneTimeKeyObject[key2].toString(); + } + oneTimeKeys.keys[key1.toString()] = keyMap; + } + return oneTimeKeys; +} + +QMap QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +{ + QMap signedOneTimeKeys; + for (const auto &keyid : keys.curve25519().keys()) { + const auto oneTimeKey = keys.curve25519()[keyid]; + QByteArray sign = signOneTimeKey(oneTimeKey); + signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); + } + return signedOneTimeKeys; +} + +SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const +{ + SignedOneTimeKey sign{}; + sign.key = key; + sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; + return sign; +} + +QByteArray QOlmAccount::signOneTimeKey(const QString &key) const +{ + QJsonDocument j(QJsonObject{{"key", key}}); + return sign(j.toJson()); +} + +OlmAccount *Quotient::QOlmAccount::data() +{ + return m_account; +} + +std::variant, OlmError> QOlmAccount::createInboundSession(const Message &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == Message::PreKey); + return QOlmSession::createInboundSession(this, preKeyMessage); +} + +std::variant, OlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == Message::PreKey); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); +} + +std::variant, OlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +{ + return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); +} + +#endif diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h new file mode 100644 index 00000000..f98d78ba --- /dev/null +++ b/lib/crypto/qolmaccount.h @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once +#ifdef Quotient_E2EE_ENABLED + +#include "crypto/e2ee.h" +#include "crypto/errors.h" +#include "crypto/session.h" +#include +#include + +struct OlmAccount; + +namespace Quotient { + +class QOlmSession; + +//! An olm account manages all cryptographic keys used on a device. +//! \code{.cpp} +//! const auto olmAccount = new QOlmAccount(this); +//! \endcode +class QOlmAccount +{ +public: + QOlmAccount(const QString &userId, const QString &deviceId); + ~QOlmAccount(); + + //! Creates a new instance of OlmAccount. During the instantiation + //! the Ed25519 fingerprint key pair and the Curve25519 identity key + //! pair are generated. For more information see here. + //! This needs to be called before any other action or use unpickle() instead. + void createNewAccount(); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. + //! This needs to be called before any other action or use createNewAccount() instead. + void unpickle(QByteArray &picked, const PicklingMode &mode); + + //! Serialises an OlmAccount to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + + //! Returns the account's public identity keys already formatted as JSON + IdentityKeys identityKeys() const; + + //! Returns the signature of the supplied message. + QByteArray sign(const QByteArray &message) const; + + //! Sign identity keys. + QByteArray signIdentityKeys() const; + + //! Maximum number of one time keys that this OlmAccount can + //! currently hold. + size_t maxNumberOfOneTimeKeys() const; + + //! Generates the supplied number of one time keys. + void generateOneTimeKeys(size_t numberOfKeys) const; + + //! Gets the OlmAccount's one time keys formatted as JSON. + OneTimeKeys oneTimeKeys() const; + + //! Sign all time key. + QMap signOneTimeKeys(const OneTimeKeys &keys) const; + + //! Sign one time key. + QByteArray signOneTimeKey(const QString &key) const; + + SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param message An Olm pre-key message that was encrypted for this account. + std::variant, OlmError> createInboundSession(const Message &preKeyMessage); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param theirIdentityKey - The identity key of an Olm account that + //! encrypted this Olm message. + std::variant, OlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage); + + //! Creates an outbound session for sending messages to a specific + /// identity and one time key. + std::variant, OlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + + // HACK do not use directly + QOlmAccount(OlmAccount *account); + OlmAccount *data(); +private: + OlmAccount *m_account = nullptr; + QString m_userId; + QString m_deviceId; +}; + +} // namespace Quotient + +#endif diff --git a/lib/crypto/qolminboundsession.cpp b/lib/crypto/qolminboundsession.cpp new file mode 100644 index 00000000..539fdc51 --- /dev/null +++ b/lib/crypto/qolminboundsession.cpp @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/qolminboundsession.h" +#include +#include +using namespace Quotient; + +OlmError lastError(OlmInboundGroupSession *session) { + const std::string error_raw = olm_inbound_group_session_last_error(session); + + std::cout << error_raw; + return fromString(error_raw); +} + +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) + : m_groupSession(session) +{ +} + +QOlmInboundGroupSession::~QOlmInboundGroupSession() +{ + olm_clear_inbound_group_session(m_groupSession); + //delete[](reinterpret_cast(m_groupSession)); +} + +std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + const auto temp = key; + const auto error = olm_init_inbound_group_session(olmInboundGroupSession, + reinterpret_cast(temp.data()), temp.size()); + + if (error == olm_error()) { + throw lastError(olmInboundGroupSession); + } + + return std::make_unique(olmInboundGroupSession); +} + + +std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray keyBuf = key; + + const auto error = olm_import_inbound_group_session(olmInboundGroupSession, + reinterpret_cast(keyBuf.data()), keyBuf.size()); + if (error == olm_error()) { + throw lastError(olmInboundGroupSession); + } + + return std::make_unique(olmInboundGroupSession); +} + +QByteArray toKey(const PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); + const QByteArray key = toKey(mode); + const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), + pickledBuf.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return pickledBuf; +} + +std::variant, OlmError> QOlmInboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.size()); + if (error == olm_error()) { + return lastError(groupSession); + } + key.clear(); + + return std::make_unique(groupSession); +} + +std::variant, OlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) +{ + // This is for capturing the output of olm_group_decrypt + uint32_t messageIndex = 0; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, + reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); + + messageBuf = QByteArray(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), + messageBuf.length(), reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); + + // Error code or plaintext length is returned + const auto decryptError = plaintextLen; + + if (decryptError == olm_error()) { + return lastError(m_groupSession); + } + + QByteArray output(plaintextLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); + + return std::make_pair(QString(output), messageIndex); +} + +std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +{ + const auto keyLen = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLen, '0'); + const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLen, messageIndex); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuf; +} + +uint32_t QOlmInboundGroupSession::firstKnownIndex() const +{ + return olm_inbound_group_session_first_known_index(m_groupSession); +} + +QByteArray QOlmInboundGroupSession::sessionId() const +{ + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); + const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), + sessionIdBuf.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return sessionIdBuf; +} + +bool QOlmInboundGroupSession::isVerified() const +{ + return olm_inbound_group_session_is_verified(m_groupSession) != 0; +} +#endif diff --git a/lib/crypto/qolminboundsession.h b/lib/crypto/qolminboundsession.h new file mode 100644 index 00000000..a58fbbbc --- /dev/null +++ b/lib/crypto/qolminboundsession.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED + +#include +#include +#include +#include "olm/olm.h" +#include "crypto/errors.h" +#include "crypto/e2ee.h" + +namespace Quotient { + +//! An in-bound group session is responsible for decrypting incoming +//! communication in a Megolm session. +struct QOlmInboundGroupSession +{ +public: + ~QOlmInboundGroupSession(); + //! Creates a new instance of `OlmInboundGroupSession`. + static std::unique_ptr create(const QByteArray &key); + //! Import an inbound group session, from a previous export. + static std::unique_ptr import(const QByteArray &key); + //! Serialises an `OlmInboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by pickling + //! an `OlmInboundGroupSession`. + static std::variant, OlmError> unpickle(QByteArray &picked, const PicklingMode &mode); + //! Decrypts ciphertext received for this group session. + std::variant, OlmError> decrypt(const QByteArray &message); + //! Export the base64-encoded ratchet key for this session, at the given index, + //! in a format which can be used by import. + std::variant exportSession(uint32_t messageIndex); + //! Get the first message index we know how to decrypt. + uint32_t firstKnownIndex() const; + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + bool isVerified() const; + QOlmInboundGroupSession(OlmInboundGroupSession *session); +private: + OlmInboundGroupSession *m_groupSession; +}; + +using QOlmInboundGroupSessionPtr = std::unique_ptr; +using OlmInboundGroupSessionPtr = std::unique_ptr; +} // namespace Quotient +#endif diff --git a/lib/crypto/qolmoutboundsession.cpp b/lib/crypto/qolmoutboundsession.cpp new file mode 100644 index 00000000..3bfb0187 --- /dev/null +++ b/lib/crypto/qolmoutboundsession.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/qolmoutboundsession.h" +#include "crypto/utils.h" + +using namespace Quotient; + +OlmError lastError(OlmOutboundGroupSession *session) { + const std::string error_raw = olm_outbound_group_session_last_error(session); + + return fromString(error_raw); +} + +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) + : m_groupSession(session) +{ +} + +QOlmOutboundGroupSession::~QOlmOutboundGroupSession() +{ + olm_clear_outbound_group_session(m_groupSession); + delete[](reinterpret_cast(m_groupSession)); +} + +std::unique_ptr QOlmOutboundGroupSession::create() +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + const auto randomLen = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); + QByteArray randomBuf = getRandom(randomLen); + + const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + throw lastError(olmOutboundGroupSession); + } + + const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); + + randomBuf.clear(); + + return std::make_unique(olmOutboundGroupSession); +} + +std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + key.clear(); + + return pickledBuf; +} + + +std::variant, OlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), + pickled.data(), pickled.length()); + if (error == olm_error()) { + return lastError(olmOutboundGroupSession); + } + const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); + QByteArray idBuffer(idMaxLength, '0'); + olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + + key.clear(); + return std::make_unique(olmOutboundGroupSession); +} + +std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), + plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + return messageBuf; +} + +uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const +{ + return olm_outbound_group_session_message_index(m_groupSession); +} + +QByteArray QOlmOutboundGroupSession::sessionId() const +{ + const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return idBuffer; +} + +std::variant QOlmOutboundGroupSession::sessionKey() const +{ + const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuffer; +} + +#endif diff --git a/lib/crypto/qolmoutboundsession.h b/lib/crypto/qolmoutboundsession.h new file mode 100644 index 00000000..6c6c635c --- /dev/null +++ b/lib/crypto/qolmoutboundsession.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once +#ifdef Quotient_E2EE_ENABLED + +#include "olm/olm.h" // from Olm +#include "crypto/errors.h" +#include "crypto/e2ee.h" +#include + +namespace Quotient { + + +//! An out-bound group session is responsible for encrypting outgoing +//! communication in a Megolm session. +class QOlmOutboundGroupSession +{ +public: + ~QOlmOutboundGroupSession(); + //! Creates a new instance of `QOlmOutboundGroupSession`. + //! Throw OlmError on errors + static std::unique_ptr create(); + //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by + //! pickling a `QOlmOutboundGroupSession`. + static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + std::variant encrypt(const QString &plaintext); + + //! Get the current message index for this session. + //! + //! Each message is sent with an increasing index; this returns the + //! index for the next message. + uint32_t sessionMessageIndex() const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Get the base64-encoded current ratchet key for this session. + //! + //! Each message is sent with a different ratchet key. This function returns the + //! ratchet key that will be used for the next message. + std::variant sessionKey() const; + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); +private: + OlmOutboundGroupSession *m_groupSession; +}; + +using QOlmOutboundGroupSessionPtr = std::unique_ptr; +using OlmOutboundGroupSessionPtr = std::unique_ptr; +} +#endif diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp new file mode 100644 index 00000000..afa42728 --- /dev/null +++ b/lib/crypto/qolmsession.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "crypto/qolmsession.h" + +using namespace Quotient; + +std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext) +{ + if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) { + return PreKeyMessage { ciphertext }; + } else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) { + return Message { ciphertext }; + } + return std::nullopt; +} + +std::pair toPair(const OlmMessage &message) +{ + return std::visit([](auto &arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::make_pair(MessageType, QByteArray(arg.message)); + } else if constexpr (std::is_same_v) { + return std::make_pair(PreKeyType, QByteArray(arg.message)); + } + }, message); +} diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h new file mode 100644 index 00000000..3be3c7fc --- /dev/null +++ b/lib/crypto/qolmsession.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include "crypto/e2ee.h" +#include "crypto/errors.h" + +namespace Quotient { + +//! An encrypted Olm message. +struct Message { + QByteArray message; +}; + +//! A encrypted Olm pre-key message. +//! +//! This message, unlike a normal Message, can be used to create new Olm sessions. +struct PreKeyMessage +{ + QByteArray message; +}; + +enum OlmMessageType +{ + PreKeyType, + MessageType, +}; + +using OlmMessage = std::variant; + +std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext); + +std::pair toPair(const OlmMessage &message); + +//class QOlmSession +//{ +// /// Creates an inbound session for sending/receiving messages from a received 'prekey' message. +// static std::variant, OlmError> createInboundSession(const QOlmAccount &account, +// PreKeyMessage &message); +// +////private: +// //static std::variant, OlmError> createSessionWith(std::function> func); +//} + +} diff --git a/lib/crypto/session.cpp b/lib/crypto/session.cpp new file mode 100644 index 00000000..8b2cb022 --- /dev/null +++ b/lib/crypto/session.cpp @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/session.h" +#include "crypto/utils.h" +#include "logging.h" +#include + +using namespace Quotient; + +OlmError lastError(OlmSession* session) { + const std::string error_raw = olm_session_last_error(session); + + return fromString(error_raw); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); + delete[](reinterpret_cast(m_session)); +} + +OlmSession* QOlmSession::create() +{ + return olm_session(new uint8_t[olm_session_size()]); +} + +std::variant, OlmError> QOlmSession::createInbound(QOlmAccount *account, const Message &preKeyMessage, bool from, const QString &theirIdentityKey) +{ + if (preKeyMessage.type() != Message::PreKey) { + qCDebug(E2EE) << "The message is not a pre-key"; + throw BadMessageFormat; + } + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + const auto lastErr = lastError(olmSession); + if (lastErr == OlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; + } + + return std::make_unique(olmSession); +} + +std::variant, OlmError> QOlmSession::createInboundSession(QOlmAccount *account, const Message &preKeyMessage) +{ + return createInbound(account, preKeyMessage); +} + +std::variant, OlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +std::variant, OlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +{ + auto *olmOutboundSession = create(); + const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); + QByteArray randomBuf = getRandom(randomLen); + + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); + const auto error = olm_create_outbound_session(olmOutboundSession, + account->data(), + reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), + reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + const auto lastErr = lastError(olmOutboundSession); + if (lastErr == OlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; + } + + randomBuf.clear(); + return std::make_unique(olmOutboundSession); +} + +std::variant QOlmSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + + key.clear(); + + return pickledBuf; +} + +std::variant, OlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmSession = create(); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + if (error == olm_error()) { + return lastError(olmSession); + } + + key.clear(); + return std::make_unique(olmSession); +} + +Message QOlmSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto messageType = encryptMessageType(); + const auto randomLen = olm_encrypt_random_length(m_session); + QByteArray randomBuf = getRandom(randomLen); + const auto error = olm_encrypt(m_session, + reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length(), + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + throw lastError(m_session); + } + + return Message(messageBuf, messageType); +} + +std::variant QOlmSession::decrypt(const Message &message) const +{ + const auto messageType = message.type(); + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = messageType == Message::Type::General + ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (plaintextMaxLen == olm_error()) { + return lastError(m_session); + } + + QByteArray plaintextBuf(plaintextMaxLen, '0'); + QByteArray messageBuf2(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, + reinterpret_cast(messageBuf2.data()), messageBuf2.length(), + reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); + + if (plaintextResultLen == olm_error()) { + const auto lastErr = lastError(m_session); + if (lastErr == OlmError::OutputBufferTooSmall) { + throw lastErr; + } + return lastErr; + } + QByteArray output(plaintextResultLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); + plaintextBuf.clear(); + return output; +} + +Message::Type QOlmSession::encryptMessageType() +{ + const auto messageTypeResult = olm_encrypt_message_type(m_session); + if (messageTypeResult == olm_error()) { + throw lastError(m_session); + } + if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { + return Message::PreKey; + } + return Message::General; +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_session); + } + return idBuffer; +} + +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + +std::variant QOlmSession::matchesInboundSession(Message &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == Message::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + + if (matchesResult == olm_error()) { + return lastError(m_session); + } + switch (matchesResult) { + case 0: + return false; + case 1: + return true; + default: + return OlmError::Unknown; + } +} + +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) +{ +} + +#endif // Quotient_E2EE_ENABLED + + + diff --git a/lib/crypto/session.h b/lib/crypto/session.h new file mode 100644 index 00000000..24702564 --- /dev/null +++ b/lib/crypto/session.h @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED + +#include +#include +#include "crypto/e2ee.h" +#include "crypto/message.h" +#include "crypto/errors.h" +#include "crypto/qolmaccount.h" + +namespace Quotient { + +class QOlmAccount; +class QOlmSession; + + +//! Either an outbound or inbound session for secure communication. +class QOlmSession +{ +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static std::variant, OlmError> createInboundSession(QOlmAccount *account, const Message &preKeyMessage); + static std::variant, OlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage); + static std::variant, OlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); + //! Serialises an `QOlmSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. + static std::variant, OlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + Message encrypt(const QString &plaintext); + + //! Decrypts a message using this session. Decoding is lossy, meaing if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + std::variant decrypt(const Message &message) const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! The type of the next message that will be returned from encryption. + Message::Type encryptMessageType(); + + //! Checker for any received messages for this session. + bool hasReceivedMessage() const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant matchesInboundSession(Message &preKeyMessage); + + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { + return *lhs < *rhs; + } + + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static std::variant, OlmError> createInbound(QOlmAccount *account, const Message& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; + + +//using QOlmSessionPtr = std::unique_ptr; + +} //namespace Quotient + +#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/utils.cpp b/lib/crypto/utils.cpp new file mode 100644 index 00000000..cb20abf8 --- /dev/null +++ b/lib/crypto/utils.cpp @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/utils.h" +#include +#include + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +QByteArray Quotient::getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); + return buffer; +} +#endif diff --git a/lib/crypto/utils.h b/lib/crypto/utils.h new file mode 100644 index 00000000..cea87144 --- /dev/null +++ b/lib/crypto/utils.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once +#ifdef Quotient_E2EE_ENABLED + +#include "crypto/e2ee.h" + +namespace Quotient { +// Convert PicklingMode to key +QByteArray toKey(const PicklingMode &mode); +QByteArray getRandom(size_t bufferSize); +} +#endif diff --git a/lib/olm/e2ee.h b/lib/olm/e2ee.h deleted file mode 100644 index 74f876e4..00000000 --- a/lib/olm/e2ee.h +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev -// SPDX-FileCopyrightText: 2019 Kitsune Ral -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "util.h" -#include -#include -#include -#include - -#include - -namespace Quotient { -inline const auto CiphertextKeyL = "ciphertext"_ls; -inline const auto SenderKeyKeyL = "sender_key"_ls; -inline const auto DeviceIdKeyL = "device_id"_ls; -inline const auto SessionIdKeyL = "session_id"_ls; - -inline const auto AlgorithmKeyL = "algorithm"_ls; -inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; -inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; - -inline const auto AlgorithmKey = QStringLiteral("algorithm"); -inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); -inline const auto RotationPeriodMsgsKey = - QStringLiteral("rotation_period_msgs"); - -inline const auto Ed25519Key = QStringLiteral("ed25519"); -inline const auto Curve25519Key = QStringLiteral("curve25519"); -inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); -inline const auto OlmV1Curve25519AesSha2AlgoKey = - QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -inline const auto MegolmV1AesSha2AlgoKey = - QStringLiteral("m.megolm.v1.aes-sha2"); -inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, - MegolmV1AesSha2AlgoKey }; - -struct Unencrypted {}; -struct Encrypted { - QByteArray key; -}; - -using PicklingMode = std::variant; - -template struct overloaded : Ts... { using Ts::operator()...; }; -template overloaded(Ts...) -> overloaded; - -struct IdentityKeys -{ - QByteArray curve25519; - QByteArray ed25519; -}; - -//! Struct representing the one-time keys. -struct OneTimeKeys -{ - QMap> keys; - - //! Get the HashMap containing the curve25519 one-time keys. - QMap curve25519() const; - - //! Get a reference to the hashmap corresponding to given key type. - std::optional> get(QString keyType) const; -}; - -//! Struct representing the signed one-time keys. -struct SignedOneTimeKey -{ - //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. - QString key; - - //! Required. Signatures of the key object. - //! The signature is calculated using the process described at Signing JSON. - QMap> signatures; -}; - -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); - -} // namespace Quotient diff --git a/lib/olm/errors.cpp b/lib/olm/errors.cpp deleted file mode 100644 index a687e807..00000000 --- a/lib/olm/errors.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED -#include "olm/errors.h" - -Quotient::OlmError Quotient::fromString(const std::string &error_raw) { - if (error_raw.compare("BAD_ACCOUNT_KEY")) { - return OlmError::BadAccountKey; - } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { - return OlmError::BadMessageKeyId; - } else if (error_raw.compare("INVALID_BASE64")) { - return OlmError::InvalidBase64; - } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { - return OlmError::NotEnoughRandom; - } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { - return OlmError::OutputBufferTooSmall; - } else { - return OlmError::Unknown; - } -} -#endif diff --git a/lib/olm/errors.h b/lib/olm/errors.h deleted file mode 100644 index 09d2a989..00000000 --- a/lib/olm/errors.h +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED -#include - -namespace Quotient { -//! All errors that could be caused by an operation regarding Olm -//! Errors are named exactly like the ones in libolm. -enum OlmError -{ - BadAccountKey, - BadMessageFormat, - BadMessageKeyId, - BadMessageMac, - BadMessageVersion, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, - UnknownMessageIndex, - Unknown, -}; - -OlmError fromString(const std::string &error_raw); - -} //namespace Quotient - -#endif diff --git a/lib/olm/message.cpp b/lib/olm/message.cpp deleted file mode 100644 index ac7038ae..00000000 --- a/lib/olm/message.cpp +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "olm/message.h" - -using namespace Quotient; - -Message::Message(const QByteArray &ciphertext, Message::Type type) - : QByteArray(std::move(ciphertext)) - , m_messageType(type) -{ - Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); -} - -Message::Message(const Message &message) - : QByteArray(message) - , m_messageType(message.type()) -{ -} - -Message::Type Message::type() const -{ - return m_messageType; -} - -QByteArray Message::toCiphertext() const -{ - return QByteArray(*this); -} - -Message Message::fromCiphertext(const QByteArray &ciphertext) -{ - return Message(ciphertext, Message::General); -} - - -#endif // Quotient_E2EE_ENABLED - - - diff --git a/lib/olm/message.h b/lib/olm/message.h deleted file mode 100644 index d2fe871e..00000000 --- a/lib/olm/message.h +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED - -#include -#include - -namespace Quotient { - -/*! \brief A wrapper around an olm encrypted message - * - * This class encapsulates a Matrix olm encrypted message, - * passed in either of 2 forms: a general message or a pre-key message. - * - * The class provides functions to get a type and the ciphertext. - */ -class Message : public QByteArray { - Q_GADGET -public: - enum Type { - General, - PreKey, - }; - Q_ENUM(Type) - - Message() = default; - explicit Message(const QByteArray &ciphertext, Type type = General); - explicit Message(const Message &message); - - static Message fromCiphertext(const QByteArray &ciphertext); - - Q_INVOKABLE Type type() const; - Q_INVOKABLE QByteArray toCiphertext() const; - -private: - Type m_messageType = General; -}; - - -} //namespace Quotient - -#endif // Quotient_E2EE_ENABLED diff --git a/lib/olm/qolmaccount.cpp b/lib/olm/qolmaccount.cpp deleted file mode 100644 index ef51a395..00000000 --- a/lib/olm/qolmaccount.cpp +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "qolmaccount.h" -#include "olm/utils.h" -#include -#include -#include -#include - -using namespace Quotient; - -QMap OneTimeKeys::curve25519() const -{ - return keys[QStringLiteral("curve25519")]; -} - -std::optional> OneTimeKeys::get(QString keyType) const -{ - if (!keys.contains(keyType)) { - return std::nullopt; - } - return keys[keyType]; -} - -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) -{ - return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; -} - -// Convert olm error to enum -OlmError lastError(OlmAccount *account) { - const std::string error_raw = olm_account_last_error(account); - - return fromString(error_raw); -} - -QByteArray getRandom(size_t bufferSize) -{ - QByteArray buffer(bufferSize, '0'); - std::generate(buffer.begin(), buffer.end(), std::rand); - return buffer; -} - -QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId) - : m_userId(userId) - , m_deviceId(deviceId) -{ -} - -QOlmAccount::~QOlmAccount() -{ - olm_clear_account(m_account); - delete[](reinterpret_cast(m_account)); -} - -void QOlmAccount::createNewAccount() -{ - m_account = olm_account(new uint8_t[olm_account_size()]); - size_t randomSize = olm_create_account_random_length(m_account); - QByteArray randomData = getRandom(randomSize); - const auto error = olm_create_account(m_account, randomData.data(), randomSize); - if (error == olm_error()) { - throw lastError(m_account); - } -} - -void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) -{ - m_account = olm_account(new uint8_t[olm_account_size()]); - const QByteArray key = toKey(mode); - const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); - if (error == olm_error()) { - throw lastError(m_account); - } -} - -std::variant QOlmAccount::pickle(const PicklingMode &mode) -{ - const QByteArray key = toKey(mode); - const size_t pickleLength = olm_pickle_account_length(m_account); - QByteArray pickleBuffer(pickleLength, '0'); - const auto error = olm_pickle_account(m_account, key.data(), - key.length(), pickleBuffer.data(), pickleLength); - if (error == olm_error()) { - return lastError(m_account); - } - return pickleBuffer; -} - -IdentityKeys QOlmAccount::identityKeys() const -{ - const size_t keyLength = olm_account_identity_keys_length(m_account); - QByteArray keyBuffer(keyLength, '0'); - const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); - if (error == olm_error()) { - throw lastError(m_account); - } - const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); - return IdentityKeys { - key.value(QStringLiteral("curve25519")).toString().toUtf8(), - key.value(QStringLiteral("ed25519")).toString().toUtf8() - }; -} - -QByteArray QOlmAccount::sign(const QByteArray &message) const -{ - const size_t signatureLength = olm_account_signature_length(m_account); - QByteArray signatureBuffer(signatureLength, '0'); - const auto error = olm_account_sign(m_account, message.data(), message.length(), - signatureBuffer.data(), signatureLength); - - if (error == olm_error()) { - throw lastError(m_account); - } - return signatureBuffer; -} - -QByteArray QOlmAccount::signIdentityKeys() const -{ - const auto keys = identityKeys(); - const QJsonObject j{ {Curve25519Key, QString(keys.curve25519)}, {Ed25519Key, QString(keys.ed25519)} }; - QJsonDocument doc; - doc.setObject(j); - return sign(doc.toJson()); - -} - -size_t QOlmAccount::maxNumberOfOneTimeKeys() const -{ - return olm_account_max_number_of_one_time_keys(m_account); -} - -void QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const -{ - const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); - QByteArray randomBuffer = getRandom(randomLen); - const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLen); - - if (error == olm_error()) { - throw lastError(m_account); - } -} - -OneTimeKeys QOlmAccount::oneTimeKeys() const -{ - const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); - QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); - - const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); - if (error == olm_error()) { - throw lastError(m_account); - } - const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); - OneTimeKeys oneTimeKeys; - - for (const QJsonValue &key1 : json.keys()) { - auto oneTimeKeyObject = json[key1.toString()].toObject(); - auto keyMap = QMap(); - for (const QString &key2 : oneTimeKeyObject.keys()) { - keyMap[key2] = oneTimeKeyObject[key2].toString(); - } - oneTimeKeys.keys[key1.toString()] = keyMap; - } - return oneTimeKeys; -} - -QMap QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const -{ - QMap signedOneTimeKeys; - for (const auto &keyid : keys.curve25519().keys()) { - const auto oneTimeKey = keys.curve25519()[keyid]; - QByteArray sign = signOneTimeKey(oneTimeKey); - signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); - } - return signedOneTimeKeys; -} - -SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const -{ - SignedOneTimeKey sign{}; - sign.key = key; - sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; - return sign; -} - -QByteArray QOlmAccount::signOneTimeKey(const QString &key) const -{ - QJsonDocument j(QJsonObject{{"key", key}}); - return sign(j.toJson()); -} - -OlmAccount *Quotient::QOlmAccount::data() -{ - return m_account; -} - -std::variant, OlmError> QOlmAccount::createInboundSession(const Message &preKeyMessage) -{ - Q_ASSERT(preKeyMessage.type() == Message::PreKey); - return QOlmSession::createInboundSession(this, preKeyMessage); -} - -std::variant, OlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage) -{ - Q_ASSERT(preKeyMessage.type() == Message::PreKey); - return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); -} - -std::variant, OlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) -{ - return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); -} - -#endif diff --git a/lib/olm/qolmaccount.h b/lib/olm/qolmaccount.h deleted file mode 100644 index df5e1be2..00000000 --- a/lib/olm/qolmaccount.h +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later -#pragma once -#ifdef Quotient_E2EE_ENABLED - -#include "olm/e2ee.h" -#include "olm/errors.h" -#include "olm/olm.h" -#include "olm/session.h" -#include - -struct OlmAccount; - -namespace Quotient { - -class QOlmSession; - -//! An olm account manages all cryptographic keys used on a device. -//! \code{.cpp} -//! const auto olmAccount = new QOlmAccount(this); -//! \endcode -class QOlmAccount -{ -public: - QOlmAccount(const QString &userId, const QString &deviceId); - ~QOlmAccount(); - - //! Creates a new instance of OlmAccount. During the instantiation - //! the Ed25519 fingerprint key pair and the Curve25519 identity key - //! pair are generated. For more information see here. - //! This needs to be called before any other action or use unpickle() instead. - void createNewAccount(); - - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. - //! This needs to be called before any other action or use createNewAccount() instead. - void unpickle(QByteArray &picked, const PicklingMode &mode); - - //! Serialises an OlmAccount to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - - //! Returns the account's public identity keys already formatted as JSON - IdentityKeys identityKeys() const; - - //! Returns the signature of the supplied message. - QByteArray sign(const QByteArray &message) const; - - //! Sign identity keys. - QByteArray signIdentityKeys() const; - - //! Maximum number of one time keys that this OlmAccount can - //! currently hold. - size_t maxNumberOfOneTimeKeys() const; - - //! Generates the supplied number of one time keys. - void generateOneTimeKeys(size_t numberOfKeys) const; - - //! Gets the OlmAccount's one time keys formatted as JSON. - OneTimeKeys oneTimeKeys() const; - - //! Sign all time key. - QMap signOneTimeKeys(const OneTimeKeys &keys) const; - - //! Sign one time key. - QByteArray signOneTimeKey(const QString &key) const; - - SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; - - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - //! - //! \param message An Olm pre-key message that was encrypted for this account. - std::variant, OlmError> createInboundSession(const Message &preKeyMessage); - - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - //! - //! \param theirIdentityKey - The identity key of an Olm account that - //! encrypted this Olm message. - std::variant, OlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage); - - //! Creates an outbound session for sending messages to a specific - /// identity and one time key. - std::variant, OlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); - - // HACK do not use directly - QOlmAccount(OlmAccount *account); - OlmAccount *data(); -private: - OlmAccount *m_account = nullptr; - QString m_userId; - QString m_deviceId; -}; - -} // namespace Quotient - -#endif diff --git a/lib/olm/qolminboundsession.cpp b/lib/olm/qolminboundsession.cpp deleted file mode 100644 index 11558f51..00000000 --- a/lib/olm/qolminboundsession.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "olm/qolminboundsession.h" -#include -#include -using namespace Quotient; - -OlmError lastError(OlmInboundGroupSession *session) { - const std::string error_raw = olm_inbound_group_session_last_error(session); - - std::cout << error_raw; - return fromString(error_raw); -} - -QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) - : m_groupSession(session) -{ -} - -QOlmInboundGroupSession::~QOlmInboundGroupSession() -{ - olm_clear_inbound_group_session(m_groupSession); - //delete[](reinterpret_cast(m_groupSession)); -} - -std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) -{ - const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - const auto temp = key; - const auto error = olm_init_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(temp.data()), temp.size()); - - if (error == olm_error()) { - throw lastError(olmInboundGroupSession); - } - - return std::make_unique(olmInboundGroupSession); -} - - -std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) -{ - const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - QByteArray keyBuf = key; - - const auto error = olm_import_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(keyBuf.data()), keyBuf.size()); - if (error == olm_error()) { - throw lastError(olmInboundGroupSession); - } - - return std::make_unique(olmInboundGroupSession); -} - -QByteArray toKey(const PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return ""; - } - return std::get(mode).key; -} - -QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const -{ - QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); - const QByteArray key = toKey(mode); - const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), - pickledBuf.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } - return pickledBuf; -} - -std::variant, OlmError> QOlmInboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.size()); - if (error == olm_error()) { - return lastError(groupSession); - } - key.clear(); - - return std::make_unique(groupSession); -} - -std::variant, OlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) -{ - // This is for capturing the output of olm_group_decrypt - uint32_t messageIndex = 0; - - // We need to clone the message because - // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(message.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, - reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); - - messageBuf = QByteArray(message.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), - messageBuf.length(), reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); - - // Error code or plaintext length is returned - const auto decryptError = plaintextLen; - - if (decryptError == olm_error()) { - return lastError(m_groupSession); - } - - QByteArray output(plaintextLen, '0'); - std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); - - return std::make_pair(QString(output), messageIndex); -} - -std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) -{ - const auto keyLen = olm_export_inbound_group_session_length(m_groupSession); - QByteArray keyBuf(keyLen, '0'); - const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLen, messageIndex); - - if (error == olm_error()) { - return lastError(m_groupSession); - } - return keyBuf; -} - -uint32_t QOlmInboundGroupSession::firstKnownIndex() const -{ - return olm_inbound_group_session_first_known_index(m_groupSession); -} - -QByteArray QOlmInboundGroupSession::sessionId() const -{ - QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); - const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), - sessionIdBuf.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } - return sessionIdBuf; -} - -bool QOlmInboundGroupSession::isVerified() const -{ - return olm_inbound_group_session_is_verified(m_groupSession) != 0; -} -#endif diff --git a/lib/olm/qolminboundsession.h b/lib/olm/qolminboundsession.h deleted file mode 100644 index 739a8411..00000000 --- a/lib/olm/qolminboundsession.h +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED - -#include -#include -#include -#include "olm/olm.h" -#include "olm/errors.h" -#include "olm/e2ee.h" - -namespace Quotient { - -//! An in-bound group session is responsible for decrypting incoming -//! communication in a Megolm session. -struct QOlmInboundGroupSession -{ -public: - ~QOlmInboundGroupSession(); - //! Creates a new instance of `OlmInboundGroupSession`. - static std::unique_ptr create(const QByteArray &key); - //! Import an inbound group session, from a previous export. - static std::unique_ptr import(const QByteArray &key); - //! Serialises an `OlmInboundGroupSession` to encrypted Base64. - QByteArray pickle(const PicklingMode &mode) const; - //! Deserialises from encrypted Base64 that was previously obtained by pickling - //! an `OlmInboundGroupSession`. - static std::variant, OlmError> unpickle(QByteArray &picked, const PicklingMode &mode); - //! Decrypts ciphertext received for this group session. - std::variant, OlmError> decrypt(const QByteArray &message); - //! Export the base64-encoded ratchet key for this session, at the given index, - //! in a format which can be used by import. - std::variant exportSession(uint32_t messageIndex); - //! Get the first message index we know how to decrypt. - uint32_t firstKnownIndex() const; - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - bool isVerified() const; - QOlmInboundGroupSession(OlmInboundGroupSession *session); -private: - OlmInboundGroupSession *m_groupSession; -}; - -using QOlmInboundGroupSessionPtr = std::unique_ptr; -using OlmInboundGroupSessionPtr = std::unique_ptr; -} // namespace Quotient -#endif diff --git a/lib/olm/qolmoutboundsession.cpp b/lib/olm/qolmoutboundsession.cpp deleted file mode 100644 index e5c43495..00000000 --- a/lib/olm/qolmoutboundsession.cpp +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "olm/qolmoutboundsession.h" -#include "olm/utils.h" - -using namespace Quotient; - -OlmError lastError(OlmOutboundGroupSession *session) { - const std::string error_raw = olm_outbound_group_session_last_error(session); - - return fromString(error_raw); -} - -QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) - : m_groupSession(session) -{ -} - -QOlmOutboundGroupSession::~QOlmOutboundGroupSession() -{ - olm_clear_outbound_group_session(m_groupSession); - delete[](reinterpret_cast(m_groupSession)); -} - -std::unique_ptr QOlmOutboundGroupSession::create() -{ - auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - const auto randomLen = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); - QByteArray randomBuf = getRandom(randomLen); - - const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - throw lastError(olmOutboundGroupSession); - } - - const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); - QByteArray keyBuffer(keyMaxLength, '0'); - olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); - - randomBuf.clear(); - - return std::make_unique(olmOutboundGroupSession); -} - -std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) -{ - QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); - QByteArray key = toKey(mode); - const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_groupSession); - } - - key.clear(); - - return pickledBuf; -} - - -std::variant, OlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), - pickled.data(), pickled.length()); - if (error == olm_error()) { - return lastError(olmOutboundGroupSession); - } - const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); - QByteArray idBuffer(idMaxLength, '0'); - olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - - key.clear(); - return std::make_unique(olmOutboundGroupSession); -} - -std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) -{ - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLen = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLen, '0'); - const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), - plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - return lastError(m_groupSession); - } - - return messageBuf; -} - -uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const -{ - return olm_outbound_group_session_message_index(m_groupSession); -} - -QByteArray QOlmOutboundGroupSession::sessionId() const -{ - const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); - QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } - return idBuffer; -} - -std::variant QOlmOutboundGroupSession::sessionKey() const -{ - const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); - QByteArray keyBuffer(keyMaxLength, '0'); - const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); - if (error == olm_error()) { - return lastError(m_groupSession); - } - return keyBuffer; -} - -#endif diff --git a/lib/olm/qolmoutboundsession.h b/lib/olm/qolmoutboundsession.h deleted file mode 100644 index 70c4d27f..00000000 --- a/lib/olm/qolmoutboundsession.h +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later -#pragma once -#ifdef Quotient_E2EE_ENABLED - -#include "olm/olm.h" // from Olm -#include "olm/errors.h" -#include "olm/e2ee.h" -#include - -namespace Quotient { - - -//! An out-bound group session is responsible for encrypting outgoing -//! communication in a Megolm session. -class QOlmOutboundGroupSession -{ -public: - ~QOlmOutboundGroupSession(); - //! Creates a new instance of `QOlmOutboundGroupSession`. - //! Throw OlmError on errors - static std::unique_ptr create(); - //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - //! Deserialises from encrypted Base64 that was previously obtained by - //! pickling a `QOlmOutboundGroupSession`. - static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); - //! Encrypts a plaintext message using the session. - std::variant encrypt(const QString &plaintext); - - //! Get the current message index for this session. - //! - //! Each message is sent with an increasing index; this returns the - //! index for the next message. - uint32_t sessionMessageIndex() const; - - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - - //! Get the base64-encoded current ratchet key for this session. - //! - //! Each message is sent with a different ratchet key. This function returns the - //! ratchet key that will be used for the next message. - std::variant sessionKey() const; - QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); -private: - OlmOutboundGroupSession *m_groupSession; -}; - -using QOlmOutboundGroupSessionPtr = std::unique_ptr; -using OlmOutboundGroupSessionPtr = std::unique_ptr; -} -#endif diff --git a/lib/olm/qolmsession.cpp b/lib/olm/qolmsession.cpp deleted file mode 100644 index 32a108a8..00000000 --- a/lib/olm/qolmsession.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "olm/qolmsession.h" - -using namespace Quotient; - -std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext) -{ - if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) { - return PreKeyMessage { ciphertext }; - } else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) { - return Message { ciphertext }; - } - return std::nullopt; -} - -std::pair toPair(const OlmMessage &message) -{ - return std::visit([](auto &arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return std::make_pair(MessageType, QByteArray(arg.message)); - } else if constexpr (std::is_same_v) { - return std::make_pair(PreKeyType, QByteArray(arg.message)); - } - }, message); -} diff --git a/lib/olm/qolmsession.h b/lib/olm/qolmsession.h deleted file mode 100644 index 08f47331..00000000 --- a/lib/olm/qolmsession.h +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "olm/e2ee.h" -#include "olm/olm.h" -#include "olm/errors.h" -#include - -namespace Quotient { - -//! An encrypted Olm message. -struct Message { - QByteArray message; -}; - -//! A encrypted Olm pre-key message. -//! -//! This message, unlike a normal Message, can be used to create new Olm sessions. -struct PreKeyMessage -{ - QByteArray message; -}; - -enum OlmMessageType -{ - PreKeyType, - MessageType, -}; - -using OlmMessage = std::variant; - -std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext); - -std::pair toPair(const OlmMessage &message); - -//class QOlmSession -//{ -// /// Creates an inbound session for sending/receiving messages from a received 'prekey' message. -// static std::variant, OlmError> createInboundSession(const QOlmAccount &account, -// PreKeyMessage &message); -// -////private: -// //static std::variant, OlmError> createSessionWith(std::function> func); -//} - -} diff --git a/lib/olm/session.cpp b/lib/olm/session.cpp deleted file mode 100644 index 94f12db6..00000000 --- a/lib/olm/session.cpp +++ /dev/null @@ -1,242 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "olm/session.h" -#include "olm/utils.h" -#include "logging.h" -#include - -using namespace Quotient; - -OlmError lastError(OlmSession* session) { - const std::string error_raw = olm_session_last_error(session); - - return fromString(error_raw); -} - -Quotient::QOlmSession::~QOlmSession() -{ - olm_clear_session(m_session); - delete[](reinterpret_cast(m_session)); -} - -OlmSession* QOlmSession::create() -{ - return olm_session(new uint8_t[olm_session_size()]); -} - -std::variant, OlmError> QOlmSession::createInbound(QOlmAccount *account, const Message &preKeyMessage, bool from, const QString &theirIdentityKey) -{ - if (preKeyMessage.type() != Message::PreKey) { - qCDebug(E2EE) << "The message is not a pre-key"; - throw BadMessageFormat; - } - - const auto olmSession = create(); - - QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - size_t error = 0; - if (from) { - error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - } else { - error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - } - - if (error == olm_error()) { - const auto lastErr = lastError(olmSession); - if (lastErr == OlmError::NotEnoughRandom) { - throw lastErr; - } - return lastErr; - } - - return std::make_unique(olmSession); -} - -std::variant, OlmError> QOlmSession::createInboundSession(QOlmAccount *account, const Message &preKeyMessage) -{ - return createInbound(account, preKeyMessage); -} - -std::variant, OlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage) -{ - return createInbound(account, preKeyMessage, true, theirIdentityKey); -} - -std::variant, OlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) -{ - auto *olmOutboundSession = create(); - const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); - QByteArray randomBuf = getRandom(randomLen); - - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); - const auto error = olm_create_outbound_session(olmOutboundSession, - account->data(), - reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), - reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - const auto lastErr = lastError(olmOutboundSession); - if (lastErr == OlmError::NotEnoughRandom) { - throw lastErr; - } - return lastErr; - } - - randomBuf.clear(); - return std::make_unique(olmOutboundSession); -} - -std::variant QOlmSession::pickle(const PicklingMode &mode) -{ - QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); - QByteArray key = toKey(mode); - const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_session); - } - - key.clear(); - - return pickledBuf; -} - -std::variant, OlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - auto *olmSession = create(); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - if (error == olm_error()) { - return lastError(olmSession); - } - - key.clear(); - return std::make_unique(olmSession); -} - -Message QOlmSession::encrypt(const QString &plaintext) -{ - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLen, '0'); - const auto messageType = encryptMessageType(); - const auto randomLen = olm_encrypt_random_length(m_session); - QByteArray randomBuf = getRandom(randomLen); - const auto error = olm_encrypt(m_session, - reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length(), - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - throw lastError(m_session); - } - - return Message(messageBuf, messageType); -} - -std::variant QOlmSession::decrypt(const Message &message) const -{ - const auto messageType = message.type(); - const auto ciphertext = message.toCiphertext(); - const auto messageTypeValue = messageType == Message::Type::General - ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; - - // We need to clone the message because - // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(ciphertext.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (plaintextMaxLen == olm_error()) { - return lastError(m_session); - } - - QByteArray plaintextBuf(plaintextMaxLen, '0'); - QByteArray messageBuf2(ciphertext.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf2.begin()); - - const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, - reinterpret_cast(messageBuf2.data()), messageBuf2.length(), - reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); - - if (plaintextResultLen == olm_error()) { - const auto lastErr = lastError(m_session); - if (lastErr == OlmError::OutputBufferTooSmall) { - throw lastErr; - } - return lastErr; - } - QByteArray output(plaintextResultLen, '0'); - std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); - plaintextBuf.clear(); - return output; -} - -Message::Type QOlmSession::encryptMessageType() -{ - const auto messageTypeResult = olm_encrypt_message_type(m_session); - if (messageTypeResult == olm_error()) { - throw lastError(m_session); - } - if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { - return Message::PreKey; - } - return Message::General; -} - -QByteArray QOlmSession::sessionId() const -{ - const auto idMaxLength = olm_session_id_length(m_session); - QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_session); - } - return idBuffer; -} - -bool QOlmSession::hasReceivedMessage() const -{ - return olm_session_has_received_message(m_session); -} - -std::variant QOlmSession::matchesInboundSession(Message &preKeyMessage) -{ - Q_ASSERT(preKeyMessage.type() == Message::Type::PreKey); - QByteArray oneTimeKeyBuf(preKeyMessage.data()); - const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); - - if (matchesResult == olm_error()) { - return lastError(m_session); - } - switch (matchesResult) { - case 0: - return false; - case 1: - return true; - default: - return OlmError::Unknown; - } -} - -QOlmSession::QOlmSession(OlmSession *session) - : m_session(session) -{ -} - -#endif // Quotient_E2EE_ENABLED - - - diff --git a/lib/olm/session.h b/lib/olm/session.h deleted file mode 100644 index 03b3514e..00000000 --- a/lib/olm/session.h +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED - -#include "olm/e2ee.h" -#include "olm/message.h" -#include "olm/errors.h" -#include -#include "olm/qolmaccount.h" - -namespace Quotient { - -class QOlmAccount; -class QOlmSession; - - -//! Either an outbound or inbound session for secure communication. -class QOlmSession -{ -public: - ~QOlmSession(); - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::variant, OlmError> createInboundSession(QOlmAccount *account, const Message &preKeyMessage); - static std::variant, OlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage); - static std::variant, OlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); - //! Serialises an `QOlmSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant, OlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); - //! Encrypts a plaintext message using the session. - Message encrypt(const QString &plaintext); - - //! Decrypts a message using this session. Decoding is lossy, meaing if - //! the decrypted plaintext contains invalid UTF-8 symbols, they will - //! be returned as `U+FFFD` (�). - std::variant decrypt(const Message &message) const; - - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - - //! The type of the next message that will be returned from encryption. - Message::Type encryptMessageType(); - - //! Checker for any received messages for this session. - bool hasReceivedMessage() const; - - //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSession(Message &preKeyMessage); - - friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) - { - return lhs.sessionId() < rhs.sessionId(); - } - - friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { - return *lhs < *rhs; - } - - QOlmSession(OlmSession* session); -private: - //! Helper function for creating new sessions and handling errors. - static OlmSession* create(); - static std::variant, OlmError> createInbound(QOlmAccount *account, const Message& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); - OlmSession* m_session; -}; - - -//using QOlmSessionPtr = std::unique_ptr; - -} //namespace Quotient - -#endif // Quotient_E2EE_ENABLED diff --git a/lib/olm/utils.cpp b/lib/olm/utils.cpp deleted file mode 100644 index 227e6d84..00000000 --- a/lib/olm/utils.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "olm/utils.h" -#include -#include - -using namespace Quotient; - -QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return ""; - } - return std::get(mode).key; -} - -QByteArray Quotient::getRandom(size_t bufferSize) -{ - QByteArray buffer(bufferSize, '0'); - RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); - return buffer; -} -#endif diff --git a/lib/olm/utils.h b/lib/olm/utils.h deleted file mode 100644 index 85d4605b..00000000 --- a/lib/olm/utils.h +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once -#ifdef Quotient_E2EE_ENABLED - -#include "olm/e2ee.h" - -namespace Quotient { -// Convert PicklingMode to key -QByteArray toKey(const PicklingMode &mode); -QByteArray getRandom(size_t bufferSize); -} -#endif -- 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/e2ee.h | 35 ----------------------------------- lib/encryptionmanager.cpp | 2 +- lib/events/encryptedevent.h | 2 +- lib/events/encryptionevent.cpp | 2 +- lib/room.cpp | 2 +- 5 files changed, 4 insertions(+), 39 deletions(-) delete mode 100644 lib/e2ee.h diff --git a/lib/e2ee.h b/lib/e2ee.h deleted file mode 100644 index 4044aa02..00000000 --- a/lib/e2ee.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev -// SPDX-FileCopyrightText: 2019 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "util.h" - -#include - -namespace Quotient { -inline const auto CiphertextKeyL = "ciphertext"_ls; -inline const auto SenderKeyKeyL = "sender_key"_ls; -inline const auto DeviceIdKeyL = "device_id"_ls; -inline const auto SessionIdKeyL = "session_id"_ls; - -inline const auto AlgorithmKeyL = "algorithm"_ls; -inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; -inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; - -inline const auto AlgorithmKey = QStringLiteral("algorithm"); -inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); -inline const auto RotationPeriodMsgsKey = - QStringLiteral("rotation_period_msgs"); - -inline const auto Ed25519Key = QStringLiteral("ed25519"); -inline const auto Curve25519Key = QStringLiteral("curve25519"); -inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); -inline const auto OlmV1Curve25519AesSha2AlgoKey = - QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -inline const auto MegolmV1AesSha2AlgoKey = - QStringLiteral("m.megolm.v1.aes-sha2"); -inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, - MegolmV1AesSha2AlgoKey }; -} // namespace Quotient diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 37f3b7c3..569d369a 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -6,7 +6,7 @@ #include "encryptionmanager.h" #include "connection.h" -#include "e2ee.h" +#include "crypto/e2ee.h" #include "csapi/keys.h" diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index de89a7c6..1d7ea913 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -3,7 +3,7 @@ #pragma once -#include "e2ee.h" +#include "crypto/e2ee.h" #include "roomevent.h" namespace Quotient { diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index aa05a96e..d7bb953a 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -4,7 +4,7 @@ #include "encryptionevent.h" -#include "e2ee.h" +#include "crypto/e2ee.h" #include 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. --- .gitmodules | 3 - 3rdparty/libQtOlm | 1 - CMakeLists.txt | 69 ++++------- autotests/testgroupsession.cpp | 6 +- autotests/testolmaccount.cpp | 6 +- autotests/testolmsession.cpp | 2 +- lib/connection.cpp | 12 +- lib/connection.h | 8 +- lib/crypto/e2ee.h | 7 +- lib/crypto/errors.cpp | 21 ---- lib/crypto/errors.h | 31 ----- lib/crypto/message.cpp | 42 ------- lib/crypto/message.h | 46 ------- lib/crypto/qolmaccount.cpp | 23 ++-- lib/crypto/qolmaccount.h | 15 +-- lib/crypto/qolmerrors.cpp | 21 ++++ lib/crypto/qolmerrors.h | 31 +++++ lib/crypto/qolminboundsession.cpp | 8 +- lib/crypto/qolminboundsession.h | 8 +- lib/crypto/qolmmessage.cpp | 42 +++++++ lib/crypto/qolmmessage.h | 46 +++++++ lib/crypto/qolmoutboundsession.cpp | 14 +-- lib/crypto/qolmoutboundsession.h | 12 +- lib/crypto/qolmsession.cpp | 246 ++++++++++++++++++++++++++++++++++--- lib/crypto/qolmsession.cpp.back | 29 +++++ lib/crypto/qolmsession.h | 92 +++++++++----- lib/crypto/qolmsession.h.back | 49 ++++++++ lib/crypto/qolmutils.cpp | 26 ++++ lib/crypto/qolmutils.h | 17 +++ lib/crypto/session.cpp | 242 ------------------------------------ lib/crypto/session.h | 77 ------------ lib/crypto/utils.cpp | 26 ---- lib/crypto/utils.h | 15 --- lib/encryptionmanager.cpp | 70 ++++++----- lib/encryptionmanager.h | 7 +- lib/room.cpp | 25 ++-- 36 files changed, 690 insertions(+), 705 deletions(-) delete mode 160000 3rdparty/libQtOlm delete mode 100644 lib/crypto/errors.cpp delete mode 100644 lib/crypto/errors.h delete mode 100644 lib/crypto/message.cpp delete mode 100644 lib/crypto/message.h create mode 100644 lib/crypto/qolmerrors.cpp create mode 100644 lib/crypto/qolmerrors.h create mode 100644 lib/crypto/qolmmessage.cpp create mode 100644 lib/crypto/qolmmessage.h create mode 100644 lib/crypto/qolmsession.cpp.back create mode 100644 lib/crypto/qolmsession.h.back create mode 100644 lib/crypto/qolmutils.cpp create mode 100644 lib/crypto/qolmutils.h delete mode 100644 lib/crypto/session.cpp delete mode 100644 lib/crypto/session.h delete mode 100644 lib/crypto/utils.cpp delete mode 100644 lib/crypto/utils.h diff --git a/.gitmodules b/.gitmodules index eb4c1815..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "3rdparty/libQtOlm"] - path = 3rdparty/libQtOlm - url = https://gitlab.com/b0/libqtolm.git diff --git a/3rdparty/libQtOlm b/3rdparty/libQtOlm deleted file mode 160000 index f2d8e235..00000000 --- a/3rdparty/libQtOlm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f2d8e235a4af0625fdedaaf727fef5d51293bf1b diff --git a/CMakeLists.txt b/CMakeLists.txt index 40767573..8f62af68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,46 +88,26 @@ get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") if (${PROJECT_NAME}_ENABLE_E2EE) + find_package(Olm 3.2.1 REQUIRED) + set_package_properties(Olm PROPERTIES + DESCRIPTION "Implementation of the Olm and Megolm cryptographic ratchets" + URL "https://gitlab.matrix.org/matrix-org/olm" + TYPE REQUIRED + ) + if (Olm_FOUND) + message(STATUS "Using libOlm ${Olm_VERSION} at ${Olm_DIR}") + endif() + find_package(OpenSSL 1.1.0 REQUIRED) set_package_properties(OpenSSL PROPERTIES DESCRIPTION "Open source SSL and TLS implementation and cryptographic library" URL "https://www.openssl.org/" TYPE REQUIRED ) - - if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) - AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) - add_subdirectory(3rdparty/libQtOlm) - include_directories(3rdparty/libQtOlm) - if (NOT DEFINED USE_INTREE_LIBQOLM) - set (USE_INTREE_LIBQOLM 1) - endif () - endif () - if (USE_INTREE_LIBQOLM) - message( STATUS "Using in-tree libQtOlm") - find_package(Git QUIET) - if (GIT_FOUND) - execute_process(COMMAND - "${GIT_EXECUTABLE}" rev-parse -q HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm - OUTPUT_VARIABLE QTOLM_GIT_SHA1 - OUTPUT_STRIP_TRAILING_WHITESPACE) - message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") - endif (GIT_FOUND) - else () - set(SAVED_CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) - set(CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) - find_package(QtOlm 3.0.1 REQUIRED) - set_package_properties(QtOlm PROPERTIES - DESCRIPTION "QtOlm is a Qt wrapper around libOlm" - PURPOSE "libQtOlm is required to support end-to-end encryption. See also BUILDING.md" - URL "https://gitlab.com/b0/libqtolm" - ) - if (QtOlm_FOUND) - message(STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") - endif() - endif () -endif () + if (OpenSSL_FOUND) + message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}") + endif() +endif() # Set up source files list(APPEND lib_SRCS @@ -182,10 +162,10 @@ list(APPEND lib_SRCS lib/crypto/qolmsession.cpp lib/crypto/qolminboundsession.cpp lib/crypto/qolmoutboundsession.cpp - lib/crypto/utils.cpp - lib/crypto/errors.cpp - lib/crypto/session.cpp - lib/crypto/message.cpp + lib/crypto/qolmutils.cpp + lib/crypto/qolmerrors.cpp + lib/crypto/qolmsession.cpp + lib/crypto/qolmmessage.cpp ) # Configure API files generation @@ -332,16 +312,13 @@ target_include_directories(${PROJECT_NAME} PUBLIC $ ) if (${PROJECT_NAME}_ENABLE_E2EE) - target_link_libraries(${PROJECT_NAME} Olm::Olm QtOlm) - set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in + target_link_libraries(${PROJECT_NAME} Olm::Olm + OpenSSL::Crypto + OpenSSL::SSL) + set(FIND_DEPS "find_dependency(Olm OpenSSL)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} - ${Qt}::Core - ${Qt}::Network - ${Qt}::Gui - OpenSSL::Crypto - OpenSSL::SSL) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 325ca2ec..858f29d8 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -3,9 +3,9 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testgroupsession.h" -#include -#include -#include +#include "crypto/qolminboundsession.h" +#include "crypto/qolmoutboundsession.h" +#include "crypto/qolmutils.h" using namespace Quotient; diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index cbce845a..a4dfd7b5 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -3,9 +3,9 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testolmaccount.h" -#include -#include -#include +#include "crypto/qolmaccount.h" +#include "csapi/definitions/device_keys.h" +#include "events/encryptedfile.h" using namespace Quotient; diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 462c8213..6535e4fe 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include "crypto/qolmsession.h" #include "testolmsession.h" using namespace Quotient; diff --git a/lib/connection.cpp b/lib/connection.cpp index e65fdac4..f96eeb71 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -38,7 +38,7 @@ #include "jobs/syncjob.h" #ifdef Quotient_E2EE_ENABLED -# include "account.h" // QtOlm +# include "crypto/qolmaccount.h" #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -191,7 +191,7 @@ public: return {}; const auto identityKey = - encryptionManager->account()->curve25519IdentityKey(); + encryptionManager->account()->identityKeys().curve25519; const auto personalCipherObject = encryptedEvent.ciphertext(identityKey); if (personalCipherObject.isEmpty()) { @@ -203,7 +203,7 @@ public: if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" << encryptedEvent.senderKey() - << encryptionManager->account()->oneTimeKeys(); + << encryptionManager->account()->oneTimeKeys().keys; return {}; } @@ -232,10 +232,10 @@ public: .value(Ed25519Key).toString(); if (ourKey != QString::fromUtf8( - encryptionManager->account()->ed25519IdentityKey())) { + encryptionManager->account()->identityKeys().ed25519)) { qCDebug(E2EE) << "Found key" << ourKey << "instead of ours own ed25519 key" - << encryptionManager->account()->ed25519IdentityKey() + << encryptionManager->account()->identityKeys().ed25519 << "in Olm plaintext"; return {}; } @@ -1226,7 +1226,7 @@ QByteArray Connection::accessToken() const bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } #ifdef Quotient_E2EE_ENABLED -QtOlm::Account* Connection::olmAccount() const +QOlmAccount *Connection::olmAccount() const { return d->encryptionManager->account(); } diff --git a/lib/connection.h b/lib/connection.h index 05a3bb7f..6729b23d 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -21,10 +21,6 @@ #include -namespace QtOlm { -class Account; -} - Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) namespace Quotient { @@ -48,6 +44,8 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; +class QOlmAccount; + using LoginFlow = GetLoginFlowsJob::LoginFlow; /// Predefined login flows @@ -310,7 +308,7 @@ public: QByteArray accessToken() const; bool isLoggedIn() const; #ifdef Quotient_E2EE_ENABLED - QtOlm::Account* olmAccount() const; + QOlmAccount* olmAccount() const; #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/crypto/e2ee.h b/lib/crypto/e2ee.h index 74f876e4..73dd7f65 100644 --- a/lib/crypto/e2ee.h +++ b/lib/crypto/e2ee.h @@ -5,15 +5,17 @@ #pragma once -#include "util.h" #include #include #include -#include +#include #include +#include "util.h" + namespace Quotient { + inline const auto CiphertextKeyL = "ciphertext"_ls; inline const auto SenderKeyKeyL = "sender_key"_ls; inline const auto DeviceIdKeyL = "device_id"_ls; @@ -37,7 +39,6 @@ inline const auto MegolmV1AesSha2AlgoKey = QStringLiteral("m.megolm.v1.aes-sha2"); inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey }; - struct Unencrypted {}; struct Encrypted { QByteArray key; diff --git a/lib/crypto/errors.cpp b/lib/crypto/errors.cpp deleted file mode 100644 index 00ff962d..00000000 --- a/lib/crypto/errors.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED -#include "crypto/errors.h" - -Quotient::OlmError Quotient::fromString(const std::string &error_raw) { - if (error_raw.compare("BAD_ACCOUNT_KEY")) { - return OlmError::BadAccountKey; - } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { - return OlmError::BadMessageKeyId; - } else if (error_raw.compare("INVALID_BASE64")) { - return OlmError::InvalidBase64; - } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { - return OlmError::NotEnoughRandom; - } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { - return OlmError::OutputBufferTooSmall; - } else { - return OlmError::Unknown; - } -} -#endif diff --git a/lib/crypto/errors.h b/lib/crypto/errors.h deleted file mode 100644 index 09d2a989..00000000 --- a/lib/crypto/errors.h +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED -#include - -namespace Quotient { -//! All errors that could be caused by an operation regarding Olm -//! Errors are named exactly like the ones in libolm. -enum OlmError -{ - BadAccountKey, - BadMessageFormat, - BadMessageKeyId, - BadMessageMac, - BadMessageVersion, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, - UnknownMessageIndex, - Unknown, -}; - -OlmError fromString(const std::string &error_raw); - -} //namespace Quotient - -#endif diff --git a/lib/crypto/message.cpp b/lib/crypto/message.cpp deleted file mode 100644 index 830633bf..00000000 --- a/lib/crypto/message.cpp +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "crypto/message.h" - -using namespace Quotient; - -Message::Message(const QByteArray &ciphertext, Message::Type type) - : QByteArray(std::move(ciphertext)) - , m_messageType(type) -{ - Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); -} - -Message::Message(const Message &message) - : QByteArray(message) - , m_messageType(message.type()) -{ -} - -Message::Type Message::type() const -{ - return m_messageType; -} - -QByteArray Message::toCiphertext() const -{ - return QByteArray(*this); -} - -Message Message::fromCiphertext(const QByteArray &ciphertext) -{ - return Message(ciphertext, Message::General); -} - - -#endif // Quotient_E2EE_ENABLED - - - diff --git a/lib/crypto/message.h b/lib/crypto/message.h deleted file mode 100644 index 1ae19ba8..00000000 --- a/lib/crypto/message.h +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED - -#include -#include - -namespace Quotient { - -/*! \brief A wrapper around an olm encrypted message - * - * This class encapsulates a Matrix olm encrypted message, - * passed in either of 2 forms: a general message or a pre-key message. - * - * The class provides functions to get a type and the ciphertext. - */ -class Message : public QByteArray { - Q_GADGET -public: - enum Type { - General, - PreKey, - }; - Q_ENUM(Type) - - Message() = default; - explicit Message(const QByteArray &ciphertext, Type type = General); - explicit Message(const Message &message); - - static Message fromCiphertext(const QByteArray &ciphertext); - - Q_INVOKABLE Type type() const; - Q_INVOKABLE QByteArray toCiphertext() const; - -private: - Type m_messageType = General; -}; - - -} //namespace Quotient - -#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 8824e7ef..fc0fc1cf 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #ifdef Quotient_E2EE_ENABLED -#include "crypto/qolmaccount.h" -#include "crypto/utils.h" +#include "qolmaccount.h" +#include "crypto/qolmutils.h" #include #include #include @@ -31,7 +31,7 @@ bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) } // Convert olm error to enum -OlmError lastError(OlmAccount *account) { +QOlmError lastError(OlmAccount *account) { const std::string error_raw = olm_account_last_error(account); return fromString(error_raw); @@ -77,7 +77,7 @@ void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) } } -std::variant QOlmAccount::pickle(const PicklingMode &mode) +std::variant QOlmAccount::pickle(const PicklingMode &mode) { const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); @@ -118,6 +118,11 @@ QByteArray QOlmAccount::sign(const QByteArray &message) const return signatureBuffer; } +QByteArray QOlmAccount::sign(const QJsonObject &message) const +{ + return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); +} + QByteArray QOlmAccount::signIdentityKeys() const { const auto keys = identityKeys(); @@ -197,19 +202,19 @@ OlmAccount *Quotient::QOlmAccount::data() return m_account; } -std::variant, OlmError> QOlmAccount::createInboundSession(const Message &preKeyMessage) +std::variant, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) { - Q_ASSERT(preKeyMessage.type() == Message::PreKey); + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSession(this, preKeyMessage); } -std::variant, OlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage) +std::variant, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) { - Q_ASSERT(preKeyMessage.type() == Message::PreKey); + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); } -std::variant, OlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +std::variant, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) { return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); } diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index f98d78ba..b33e3768 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -5,9 +5,9 @@ #ifdef Quotient_E2EE_ENABLED #include "crypto/e2ee.h" -#include "crypto/errors.h" -#include "crypto/session.h" -#include +#include "crypto/qolmerrors.h" +#include "crypto/qolmmessage.h" +#include "crypto/qolmsession.h" #include struct OlmAccount; @@ -38,13 +38,14 @@ public: void unpickle(QByteArray &picked, const PicklingMode &mode); //! Serialises an OlmAccount to encrypted Base64. - std::variant pickle(const PicklingMode &mode); + std::variant pickle(const PicklingMode &mode); //! Returns the account's public identity keys already formatted as JSON IdentityKeys identityKeys() const; //! Returns the signature of the supplied message. QByteArray sign(const QByteArray &message) const; + QByteArray sign(const QJsonObject& message) const; //! Sign identity keys. QByteArray signIdentityKeys() const; @@ -70,17 +71,17 @@ public: //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param message An Olm pre-key message that was encrypted for this account. - std::variant, OlmError> createInboundSession(const Message &preKeyMessage); + std::variant, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param theirIdentityKey - The identity key of an Olm account that //! encrypted this Olm message. - std::variant, OlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const Message &preKeyMessage); + std::variant, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); //! Creates an outbound session for sending messages to a specific /// identity and one time key. - std::variant, OlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + std::variant, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); // HACK do not use directly QOlmAccount(OlmAccount *account); diff --git a/lib/crypto/qolmerrors.cpp b/lib/crypto/qolmerrors.cpp new file mode 100644 index 00000000..f407383e --- /dev/null +++ b/lib/crypto/qolmerrors.cpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later +#ifdef Quotient_E2EE_ENABLED +#include "qolmerrors.h" + +Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { + if (error_raw.compare("BAD_ACCOUNT_KEY")) { + return QOlmError::BadAccountKey; + } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { + return QOlmError::BadMessageKeyId; + } else if (error_raw.compare("INVALID_BASE64")) { + return QOlmError::InvalidBase64; + } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { + return QOlmError::NotEnoughRandom; + } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + return QOlmError::OutputBufferTooSmall; + } else { + return QOlmError::Unknown; + } +} +#endif diff --git a/lib/crypto/qolmerrors.h b/lib/crypto/qolmerrors.h new file mode 100644 index 00000000..400573c6 --- /dev/null +++ b/lib/crypto/qolmerrors.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED +#include + +namespace Quotient { +//! All errors that could be caused by an operation regarding Olm +//! Errors are named exactly like the ones in libolm. +enum QOlmError +{ + BadAccountKey, + BadMessageFormat, + BadMessageKeyId, + BadMessageMac, + BadMessageVersion, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + UnknownMessageIndex, + Unknown, +}; + +QOlmError fromString(const std::string &error_raw); + +} //namespace Quotient + +#endif diff --git a/lib/crypto/qolminboundsession.cpp b/lib/crypto/qolminboundsession.cpp index 539fdc51..8f5056d8 100644 --- a/lib/crypto/qolminboundsession.cpp +++ b/lib/crypto/qolminboundsession.cpp @@ -8,7 +8,7 @@ #include using namespace Quotient; -OlmError lastError(OlmInboundGroupSession *session) { +QOlmError lastError(OlmInboundGroupSession *session) { const std::string error_raw = olm_inbound_group_session_last_error(session); std::cout << error_raw; @@ -75,7 +75,7 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const return pickledBuf; } -std::variant, OlmError> QOlmInboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +std::variant, QOlmError> QOlmInboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); @@ -90,7 +90,7 @@ std::variant, OlmError> QOlmInboundGrou return std::make_unique(groupSession); } -std::variant, OlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) +std::variant, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) { // This is for capturing the output of olm_group_decrypt uint32_t messageIndex = 0; @@ -122,7 +122,7 @@ std::variant, OlmError> QOlmInboundGroupSession::de return std::make_pair(QString(output), messageIndex); } -std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) { const auto keyLen = olm_export_inbound_group_session_length(m_groupSession); QByteArray keyBuf(keyLen, '0'); diff --git a/lib/crypto/qolminboundsession.h b/lib/crypto/qolminboundsession.h index a58fbbbc..6af71cbd 100644 --- a/lib/crypto/qolminboundsession.h +++ b/lib/crypto/qolminboundsession.h @@ -10,7 +10,7 @@ #include #include #include "olm/olm.h" -#include "crypto/errors.h" +#include "crypto/qolmerrors.h" #include "crypto/e2ee.h" namespace Quotient { @@ -29,12 +29,12 @@ public: QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant, OlmError> unpickle(QByteArray &picked, const PicklingMode &mode); + static std::variant, QOlmError> unpickle(QByteArray &picked, const PicklingMode &mode); //! Decrypts ciphertext received for this group session. - std::variant, OlmError> decrypt(const QByteArray &message); + std::variant, QOlmError> decrypt(const QByteArray &message); //! Export the base64-encoded ratchet key for this session, at the given index, //! in a format which can be used by import. - std::variant exportSession(uint32_t messageIndex); + std::variant exportSession(uint32_t messageIndex); //! Get the first message index we know how to decrypt. uint32_t firstKnownIndex() const; //! Get a base64-encoded identifier for this session. diff --git a/lib/crypto/qolmmessage.cpp b/lib/crypto/qolmmessage.cpp new file mode 100644 index 00000000..ae98d52f --- /dev/null +++ b/lib/crypto/qolmmessage.cpp @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "qolmmessage.h" + +using namespace Quotient; + +QOlmMessage::QOlmMessage(const QByteArray &ciphertext, QOlmMessage::Type type) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) +{ + Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); +} + +QOlmMessage::QOlmMessage(const QOlmMessage &message) + : QByteArray(message) + , m_messageType(message.type()) +{ +} + +QOlmMessage::Type QOlmMessage::type() const +{ + return m_messageType; +} + +QByteArray QOlmMessage::toCiphertext() const +{ + return QByteArray(*this); +} + +QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) +{ + return QOlmMessage(ciphertext, QOlmMessage::General); +} + + +#endif // Quotient_E2EE_ENABLED + + + diff --git a/lib/crypto/qolmmessage.h b/lib/crypto/qolmmessage.h new file mode 100644 index 00000000..d203364d --- /dev/null +++ b/lib/crypto/qolmmessage.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED + +#include +#include + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class QOlmMessage : public QByteArray { + Q_GADGET +public: + enum Type { + General, + PreKey, + }; + Q_ENUM(Type) + + QOlmMessage() = default; + explicit QOlmMessage(const QByteArray &ciphertext, Type type = General); + explicit QOlmMessage(const QOlmMessage &message); + + static QOlmMessage fromCiphertext(const QByteArray &ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type m_messageType = General; +}; + + +} //namespace Quotient + +#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmoutboundsession.cpp b/lib/crypto/qolmoutboundsession.cpp index 3bfb0187..14b7368e 100644 --- a/lib/crypto/qolmoutboundsession.cpp +++ b/lib/crypto/qolmoutboundsession.cpp @@ -3,12 +3,12 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #ifdef Quotient_E2EE_ENABLED -#include "crypto/qolmoutboundsession.h" -#include "crypto/utils.h" +#include "qolmoutboundsession.h" +#include "crypto/qolmutils.h" using namespace Quotient; -OlmError lastError(OlmOutboundGroupSession *session) { +QOlmError lastError(OlmOutboundGroupSession *session) { const std::string error_raw = olm_outbound_group_session_last_error(session); return fromString(error_raw); @@ -48,7 +48,7 @@ std::unique_ptr QOlmOutboundGroupSession::create() return std::make_unique(olmOutboundGroupSession); } -std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) { QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); QByteArray key = toKey(mode); @@ -65,7 +65,7 @@ std::variant QOlmOutboundGroupSession::pickle(const Pickli } -std::variant, OlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); @@ -84,7 +84,7 @@ std::variant, OlmError> QOlmOutboundGr return std::make_unique(olmOutboundGroupSession); } -std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) +std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) { QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLen = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); @@ -116,7 +116,7 @@ QByteArray QOlmOutboundGroupSession::sessionId() const return idBuffer; } -std::variant QOlmOutboundGroupSession::sessionKey() const +std::variant QOlmOutboundGroupSession::sessionKey() const { const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); QByteArray keyBuffer(keyMaxLength, '0'); diff --git a/lib/crypto/qolmoutboundsession.h b/lib/crypto/qolmoutboundsession.h index 6c6c635c..6b4fd30b 100644 --- a/lib/crypto/qolmoutboundsession.h +++ b/lib/crypto/qolmoutboundsession.h @@ -4,8 +4,8 @@ #pragma once #ifdef Quotient_E2EE_ENABLED -#include "olm/olm.h" // from Olm -#include "crypto/errors.h" +#include "olm/olm.h" +#include "crypto/qolmerrors.h" #include "crypto/e2ee.h" #include @@ -22,12 +22,12 @@ public: //! Throw OlmError on errors static std::unique_ptr create(); //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); + std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant, OlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + static std::variant, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. - std::variant encrypt(const QString &plaintext); + std::variant encrypt(const QString &plaintext); //! Get the current message index for this session. //! @@ -42,7 +42,7 @@ public: //! //! Each message is sent with a different ratchet key. This function returns the //! ratchet key that will be used for the next message. - std::variant sessionKey() const; + std::variant sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); private: OlmOutboundGroupSession *m_groupSession; diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp index afa42728..cfe21650 100644 --- a/lib/crypto/qolmsession.cpp +++ b/lib/crypto/qolmsession.cpp @@ -1,29 +1,243 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-FileCopyrightText: 2021 Alexey Andreyev // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "crypto/qolmsession.h" +#ifdef Quotient_E2EE_ENABLED +#include "qolmsession.h" +#include "crypto/qolmutils.h" +#include "logging.h" +#include +#include using namespace Quotient; -std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext) +QOlmError lastError(OlmSession* session) { + const std::string error_raw = olm_session_last_error(session); + + return fromString(error_raw); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); + delete[](reinterpret_cast(m_session)); +} + +OlmSession* QOlmSession::create() { - if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) { - return PreKeyMessage { ciphertext }; - } else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) { - return Message { ciphertext }; + return olm_session(new uint8_t[olm_session_size()]); +} + +std::variant, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) +{ + if (preKeyMessage.type() != QOlmMessage::PreKey) { + qCDebug(E2EE) << "The message is not a pre-key"; + throw BadMessageFormat; } - return std::nullopt; + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + const auto lastErr = lastError(olmSession); + if (lastErr == QOlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; + } + + return std::make_unique(olmSession); } -std::pair toPair(const OlmMessage &message) +std::variant, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) { - return std::visit([](auto &arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return std::make_pair(MessageType, QByteArray(arg.message)); - } else if constexpr (std::is_same_v) { - return std::make_pair(PreKeyType, QByteArray(arg.message)); + return createInbound(account, preKeyMessage); +} + +std::variant, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +std::variant, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +{ + auto *olmOutboundSession = create(); + const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); + QByteArray randomBuf = getRandom(randomLen); + + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); + const auto error = olm_create_outbound_session(olmOutboundSession, + account->data(), + reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), + reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + const auto lastErr = lastError(olmOutboundSession); + if (lastErr == QOlmError::NotEnoughRandom) { + throw lastErr; } - }, message); + return lastErr; + } + + randomBuf.clear(); + return std::make_unique(olmOutboundSession); +} + +std::variant QOlmSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + + key.clear(); + + return pickledBuf; +} + +std::variant, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmSession = create(); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + if (error == olm_error()) { + return lastError(olmSession); + } + + key.clear(); + return std::make_unique(olmSession); } + +QOlmMessage QOlmSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto messageType = encryptMessageType(); + const auto randomLen = olm_encrypt_random_length(m_session); + QByteArray randomBuf = getRandom(randomLen); + const auto error = olm_encrypt(m_session, + reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length(), + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + throw lastError(m_session); + } + + return QOlmMessage(messageBuf, messageType); +} + +std::variant QOlmSession::decrypt(const QOlmMessage &message) const +{ + const auto messageType = message.type(); + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = messageType == QOlmMessage::Type::General + ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (plaintextMaxLen == olm_error()) { + return lastError(m_session); + } + + QByteArray plaintextBuf(plaintextMaxLen, '0'); + QByteArray messageBuf2(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, + reinterpret_cast(messageBuf2.data()), messageBuf2.length(), + reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); + + if (plaintextResultLen == olm_error()) { + const auto lastErr = lastError(m_session); + if (lastErr == QOlmError::OutputBufferTooSmall) { + throw lastErr; + } + return lastErr; + } + QByteArray output(plaintextResultLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); + plaintextBuf.clear(); + return output; +} + +QOlmMessage::Type QOlmSession::encryptMessageType() +{ + const auto messageTypeResult = olm_encrypt_message_type(m_session); + if (messageTypeResult == olm_error()) { + throw lastError(m_session); + } + if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { + return QOlmMessage::PreKey; + } + return QOlmMessage::General; +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_session); + } + return idBuffer; +} + +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + +std::variant QOlmSession::matchesInboundSession(QOlmMessage &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + + if (matchesResult == olm_error()) { + return lastError(m_session); + } + switch (matchesResult) { + case 0: + return false; + case 1: + return true; + default: + return QOlmError::Unknown; + } +} + +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) +{ +} + +#endif // Quotient_E2EE_ENABLED + + + diff --git a/lib/crypto/qolmsession.cpp.back b/lib/crypto/qolmsession.cpp.back new file mode 100644 index 00000000..ee8b2a7f --- /dev/null +++ b/lib/crypto/qolmsession.cpp.back @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "olm/qolmsession.h" + +using namespace Quotient; + +std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext) +{ + if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) { + return PreKeyMessage { ciphertext }; + } else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) { + return QOlmMessage { ciphertext }; + } + return std::nullopt; +} + +std::pair toPair(const OlmMessage &message) +{ + return std::visit([](auto &arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::make_pair(MessageType, QByteArray(arg.message)); + } else if constexpr (std::is_same_v) { + return std::make_pair(PreKeyType, QByteArray(arg.message)); + } + }, message); +} diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h index 3be3c7fc..6e13801e 100644 --- a/lib/crypto/qolmsession.h +++ b/lib/crypto/qolmsession.h @@ -1,49 +1,77 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-FileCopyrightText: 2021 Alexey Andreyev // // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include -#include +#ifdef Quotient_E2EE_ENABLED + +#include +#include // FIXME: OlmSession #include "crypto/e2ee.h" -#include "crypto/errors.h" +#include "crypto/qolmmessage.h" +#include "crypto/qolmerrors.h" +#include "crypto/qolmaccount.h" namespace Quotient { -//! An encrypted Olm message. -struct Message { - QByteArray message; -}; +class QOlmAccount; +class QOlmSession; -//! A encrypted Olm pre-key message. -//! -//! This message, unlike a normal Message, can be used to create new Olm sessions. -struct PreKeyMessage -{ - QByteArray message; -}; -enum OlmMessageType +//! Either an outbound or inbound session for secure communication. +class QOlmSession { - PreKeyType, - MessageType, -}; +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static std::variant, QOlmError> createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage); + static std::variant, QOlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage); + static std::variant, QOlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); + //! Serialises an `QOlmSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. + static std::variant, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + QOlmMessage encrypt(const QString &plaintext); -using OlmMessage = std::variant; + //! Decrypts a message using this session. Decoding is lossy, meaing if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + std::variant decrypt(const QOlmMessage &message) const; -std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext); + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; -std::pair toPair(const OlmMessage &message); + //! The type of the next message that will be returned from encryption. + QOlmMessage::Type encryptMessageType(); -//class QOlmSession -//{ -// /// Creates an inbound session for sending/receiving messages from a received 'prekey' message. -// static std::variant, OlmError> createInboundSession(const QOlmAccount &account, -// PreKeyMessage &message); -// -////private: -// //static std::variant, OlmError> createSessionWith(std::function> func); -//} + //! Checker for any received messages for this session. + bool hasReceivedMessage() const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant matchesInboundSession(QOlmMessage &preKeyMessage); + + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { + return *lhs < *rhs; + } + + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static std::variant, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; + + +//using QOlmSessionPtr = std::unique_ptr; + +} //namespace Quotient -} +#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmsession.h.back b/lib/crypto/qolmsession.h.back new file mode 100644 index 00000000..cbba5cef --- /dev/null +++ b/lib/crypto/qolmsession.h.back @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "olm/e2ee.h" +#include "olm/olm.h" +#include "olm/errors.h" +#include + +namespace Quotient { + +//! An encrypted Olm message. +struct QOlmMessage { + QByteArray message; +}; + +//! A encrypted Olm pre-key message. +//! +//! This message, unlike a normal Message, can be used to create new Olm sessions. +struct PreKeyMessage +{ + QByteArray message; +}; + +enum OlmMessageType +{ + PreKeyType, + MessageType, +}; + +using OlmMessage = std::variant; + +std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext); + +std::pair toPair(const OlmMessage &message); + +//class QOlmSession +//{ +// /// Creates an inbound session for sending/receiving messages from a received 'prekey' message. +// static std::variant, OlmError> createInboundSession(const QOlmAccount &account, +// PreKeyMessage &message); +// +////private: +// //static std::variant, OlmError> createSessionWith(std::function> func); +//} + +} diff --git a/lib/crypto/qolmutils.cpp b/lib/crypto/qolmutils.cpp new file mode 100644 index 00000000..a486ea0f --- /dev/null +++ b/lib/crypto/qolmutils.cpp @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/qolmutils.h" +#include +#include + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +QByteArray Quotient::getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); + return buffer; +} +#endif diff --git a/lib/crypto/qolmutils.h b/lib/crypto/qolmutils.h new file mode 100644 index 00000000..11e9f3cc --- /dev/null +++ b/lib/crypto/qolmutils.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once +#ifdef Quotient_E2EE_ENABLED + +#include + +#include "crypto/e2ee.h" + +namespace Quotient { +// Convert PicklingMode to key +QByteArray toKey(const PicklingMode &mode); +QByteArray getRandom(size_t bufferSize); +} +#endif diff --git a/lib/crypto/session.cpp b/lib/crypto/session.cpp deleted file mode 100644 index 8b2cb022..00000000 --- a/lib/crypto/session.cpp +++ /dev/null @@ -1,242 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "crypto/session.h" -#include "crypto/utils.h" -#include "logging.h" -#include - -using namespace Quotient; - -OlmError lastError(OlmSession* session) { - const std::string error_raw = olm_session_last_error(session); - - return fromString(error_raw); -} - -Quotient::QOlmSession::~QOlmSession() -{ - olm_clear_session(m_session); - delete[](reinterpret_cast(m_session)); -} - -OlmSession* QOlmSession::create() -{ - return olm_session(new uint8_t[olm_session_size()]); -} - -std::variant, OlmError> QOlmSession::createInbound(QOlmAccount *account, const Message &preKeyMessage, bool from, const QString &theirIdentityKey) -{ - if (preKeyMessage.type() != Message::PreKey) { - qCDebug(E2EE) << "The message is not a pre-key"; - throw BadMessageFormat; - } - - const auto olmSession = create(); - - QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - size_t error = 0; - if (from) { - error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - } else { - error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - } - - if (error == olm_error()) { - const auto lastErr = lastError(olmSession); - if (lastErr == OlmError::NotEnoughRandom) { - throw lastErr; - } - return lastErr; - } - - return std::make_unique(olmSession); -} - -std::variant, OlmError> QOlmSession::createInboundSession(QOlmAccount *account, const Message &preKeyMessage) -{ - return createInbound(account, preKeyMessage); -} - -std::variant, OlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage) -{ - return createInbound(account, preKeyMessage, true, theirIdentityKey); -} - -std::variant, OlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) -{ - auto *olmOutboundSession = create(); - const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); - QByteArray randomBuf = getRandom(randomLen); - - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); - const auto error = olm_create_outbound_session(olmOutboundSession, - account->data(), - reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), - reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - const auto lastErr = lastError(olmOutboundSession); - if (lastErr == OlmError::NotEnoughRandom) { - throw lastErr; - } - return lastErr; - } - - randomBuf.clear(); - return std::make_unique(olmOutboundSession); -} - -std::variant QOlmSession::pickle(const PicklingMode &mode) -{ - QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); - QByteArray key = toKey(mode); - const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_session); - } - - key.clear(); - - return pickledBuf; -} - -std::variant, OlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - auto *olmSession = create(); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - if (error == olm_error()) { - return lastError(olmSession); - } - - key.clear(); - return std::make_unique(olmSession); -} - -Message QOlmSession::encrypt(const QString &plaintext) -{ - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLen, '0'); - const auto messageType = encryptMessageType(); - const auto randomLen = olm_encrypt_random_length(m_session); - QByteArray randomBuf = getRandom(randomLen); - const auto error = olm_encrypt(m_session, - reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length(), - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - throw lastError(m_session); - } - - return Message(messageBuf, messageType); -} - -std::variant QOlmSession::decrypt(const Message &message) const -{ - const auto messageType = message.type(); - const auto ciphertext = message.toCiphertext(); - const auto messageTypeValue = messageType == Message::Type::General - ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; - - // We need to clone the message because - // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(ciphertext.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (plaintextMaxLen == olm_error()) { - return lastError(m_session); - } - - QByteArray plaintextBuf(plaintextMaxLen, '0'); - QByteArray messageBuf2(ciphertext.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf2.begin()); - - const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, - reinterpret_cast(messageBuf2.data()), messageBuf2.length(), - reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); - - if (plaintextResultLen == olm_error()) { - const auto lastErr = lastError(m_session); - if (lastErr == OlmError::OutputBufferTooSmall) { - throw lastErr; - } - return lastErr; - } - QByteArray output(plaintextResultLen, '0'); - std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); - plaintextBuf.clear(); - return output; -} - -Message::Type QOlmSession::encryptMessageType() -{ - const auto messageTypeResult = olm_encrypt_message_type(m_session); - if (messageTypeResult == olm_error()) { - throw lastError(m_session); - } - if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { - return Message::PreKey; - } - return Message::General; -} - -QByteArray QOlmSession::sessionId() const -{ - const auto idMaxLength = olm_session_id_length(m_session); - QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_session); - } - return idBuffer; -} - -bool QOlmSession::hasReceivedMessage() const -{ - return olm_session_has_received_message(m_session); -} - -std::variant QOlmSession::matchesInboundSession(Message &preKeyMessage) -{ - Q_ASSERT(preKeyMessage.type() == Message::Type::PreKey); - QByteArray oneTimeKeyBuf(preKeyMessage.data()); - const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); - - if (matchesResult == olm_error()) { - return lastError(m_session); - } - switch (matchesResult) { - case 0: - return false; - case 1: - return true; - default: - return OlmError::Unknown; - } -} - -QOlmSession::QOlmSession(OlmSession *session) - : m_session(session) -{ -} - -#endif // Quotient_E2EE_ENABLED - - - diff --git a/lib/crypto/session.h b/lib/crypto/session.h deleted file mode 100644 index 24702564..00000000 --- a/lib/crypto/session.h +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#ifdef Quotient_E2EE_ENABLED - -#include -#include -#include "crypto/e2ee.h" -#include "crypto/message.h" -#include "crypto/errors.h" -#include "crypto/qolmaccount.h" - -namespace Quotient { - -class QOlmAccount; -class QOlmSession; - - -//! Either an outbound or inbound session for secure communication. -class QOlmSession -{ -public: - ~QOlmSession(); - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::variant, OlmError> createInboundSession(QOlmAccount *account, const Message &preKeyMessage); - static std::variant, OlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const Message &preKeyMessage); - static std::variant, OlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); - //! Serialises an `QOlmSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant, OlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); - //! Encrypts a plaintext message using the session. - Message encrypt(const QString &plaintext); - - //! Decrypts a message using this session. Decoding is lossy, meaing if - //! the decrypted plaintext contains invalid UTF-8 symbols, they will - //! be returned as `U+FFFD` (�). - std::variant decrypt(const Message &message) const; - - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - - //! The type of the next message that will be returned from encryption. - Message::Type encryptMessageType(); - - //! Checker for any received messages for this session. - bool hasReceivedMessage() const; - - //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSession(Message &preKeyMessage); - - friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) - { - return lhs.sessionId() < rhs.sessionId(); - } - - friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { - return *lhs < *rhs; - } - - QOlmSession(OlmSession* session); -private: - //! Helper function for creating new sessions and handling errors. - static OlmSession* create(); - static std::variant, OlmError> createInbound(QOlmAccount *account, const Message& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); - OlmSession* m_session; -}; - - -//using QOlmSessionPtr = std::unique_ptr; - -} //namespace Quotient - -#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/utils.cpp b/lib/crypto/utils.cpp deleted file mode 100644 index cb20abf8..00000000 --- a/lib/crypto/utils.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "crypto/utils.h" -#include -#include - -using namespace Quotient; - -QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return ""; - } - return std::get(mode).key; -} - -QByteArray Quotient::getRandom(size_t bufferSize) -{ - QByteArray buffer(bufferSize, '0'); - RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); - return buffer; -} -#endif diff --git a/lib/crypto/utils.h b/lib/crypto/utils.h deleted file mode 100644 index cea87144..00000000 --- a/lib/crypto/utils.h +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once -#ifdef Quotient_E2EE_ENABLED - -#include "crypto/e2ee.h" - -namespace Quotient { -// Convert PicklingMode to key -QByteArray toKey(const PicklingMode &mode); -QByteArray getRandom(size_t bufferSize); -} -#endif diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 569d369a..8081f788 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -13,16 +13,15 @@ #include #include -#include // QtOlm -#include // QtOlm -#include // QtOlm -#include // QtOlm -#include // QtOlm +#include "crypto/qolmaccount.h" +#include "crypto/qolmsession.h" +#include "crypto/qolmmessage.h" +#include "crypto/qolmerrors.h" +#include "crypto/qolmutils.h" #include #include using namespace Quotient; -using namespace QtOlm; using std::move; class EncryptionManager::Private { @@ -36,11 +35,9 @@ public: Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); if (encryptionAccountPickle.isEmpty()) { - olmAccount.reset(new Account()); + // new e2ee TODO: olmAccount.reset(new QOlmAccount()); } else { - olmAccount.reset( - new Account(encryptionAccountPickle)); // TODO: passphrase even - // with qtkeychain? + // new e2ee TODO: olmAccount.reset(new QOlmAccount(encryptionAccountPickle)); // TODO: passphrase even with qtkeychain? } /* * Note about targetKeysNumber: @@ -54,7 +51,7 @@ public: * until the limit is reached and it starts discarding keys, starting by * the oldest. */ - targetKeysNumber = olmAccount->maxOneTimeKeys() / 2; + targetKeysNumber = olmAccount->maxNumberOfOneTimeKeys() / 2; targetOneTimeKeyCounts = { { SignedCurve25519Key, qRound(signedKeysProportion * targetKeysNumber) }, @@ -72,7 +69,7 @@ public: UploadKeysJob* uploadOneTimeKeysJob = nullptr; QueryKeysJob* queryKeysJob = nullptr; - QScopedPointer olmAccount; + QScopedPointer olmAccount; float signedKeysProportion; float oneTimeKeyThreshold; @@ -91,7 +88,7 @@ public: QHash targetOneTimeKeyCounts; // A map from senderKey to InboundSession - QMap sessions; // TODO: cache + QMap sessions; // TODO: cache void updateDeviceKeys( const QHash>& deviceKeys) @@ -103,13 +100,15 @@ public: } } } - QString sessionDecrypt(Message* message, const QString& senderKey) + QString sessionDecrypt(QOlmMessage* message, const QString& senderKey) { QString decrypted; - QList senderSessions = sessions.values(senderKey); + QList senderSessions = sessions.values(senderKey); // Try to decrypt message body using one of the known sessions for that // device bool sessionsPassed = false; + // new e2ee TODO: + /* for (auto senderSession : senderSessions) { if (senderSession == senderSessions.last()) { sessionsPassed = true; @@ -120,11 +119,9 @@ public: << "Success decrypting Olm event using existing session" << senderSession->id(); break; - } catch (OlmError* e) { - if (message->messageType() == 0) { - PreKeyMessage preKeyMessage = - PreKeyMessage(message->cipherText()); - if (senderSession->matches(&preKeyMessage, senderKey)) { + } catch (QOlmError* e) { + if (message->type() == QOlmMessage::PreKey) { + if (senderSession->matches(&message, senderKey)) { // We had a matching session for a pre-key message, but // it didn't work. This means something is wrong, so we // fail now. @@ -138,8 +135,9 @@ public: // Simply keep trying otherwise } } + */ if (sessionsPassed || senderSessions.empty()) { - if (message->messageType() > 0) { + if (message->type() != QOlmMessage::PreKey) { // Not a pre-key message, we should have had a matching session if (!sessions.empty()) { qCDebug(E2EE) << "Error decrypting with existing sessions"; @@ -150,9 +148,11 @@ public: } // We have a pre-key message without any matching session, in this // case we should try to create one. - InboundSession* newSession; + QOlmSession* newSession; qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; - PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText()); + QOlmMessage preKeyMessage = QOlmMessage(message->toCiphertext(),QOlmMessage::PreKey); + // new e2ee TODO: + /* try { newSession = new InboundSession(olmAccount.data(), &preKeyMessage, @@ -172,7 +172,9 @@ public: << e->what(); return QString(); } + olmAccount->removeOneTimeKeys(newSession); + */ sessions.insert(senderKey, newSession); } return decrypted; @@ -211,9 +213,9 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection) * as specified by the key algorithm. */ { { Curve25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->curve25519IdentityKey() }, + d->olmAccount->identityKeys().curve25519 }, { Ed25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->ed25519IdentityKey() } }, + d->olmAccount->identityKeys().curve25519 } }, /* signatures should be provided after the unsigned deviceKeys generation */ {} @@ -262,8 +264,7 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, + unsignedKeysToUploadCount); QHash oneTimeKeys = {}; - const auto& olmAccountCurve25519OneTimeKeys = - d->olmAccount->curve25519OneTimeKeys(); + const auto& olmAccountCurve25519OneTimeKeys = d->olmAccount->oneTimeKeys().curve25519(); int oneTimeKeysCounter = 0; for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); @@ -273,7 +274,7 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, QVariant key; if (oneTimeKeysCounter < signedKeysToUploadCount) { QJsonObject message { { QStringLiteral("key"), - it.value().toString() } }; + it.value() } }; QByteArray signedMessage = d->olmAccount->sign(message); QJsonObject signatures { @@ -297,7 +298,7 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] { d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts()); }); - d->olmAccount->markKeysAsPublished(); + // new e2ee TODO: d->olmAccount->markKeysAsPublished(); qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") .arg(signedKeysToUploadCount) .arg(unsignedKeysToUploadCount); @@ -328,11 +329,11 @@ QString EncryptionManager::sessionDecryptMessage( int type = personalCipherObject.value(TypeKeyL).toInt(-1); QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); if (type == 0) { - PreKeyMessage preKeyMessage { body }; - decrypted = d->sessionDecrypt(reinterpret_cast(&preKeyMessage), + QOlmMessage preKeyMessage = QOlmMessage(body, QOlmMessage::PreKey); + decrypted = d->sessionDecrypt(reinterpret_cast(&preKeyMessage), senderKey); } else if (type == 1) { - Message message { body }; + QOlmMessage message = QOlmMessage(body, QOlmMessage::PreKey); decrypted = d->sessionDecrypt(&message, senderKey); } return decrypted; @@ -340,10 +341,11 @@ QString EncryptionManager::sessionDecryptMessage( QByteArray EncryptionManager::olmAccountPickle() { - return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? + // new e2ee TODO: return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? + return {}; } -QtOlm::Account* EncryptionManager::account() const +QOlmAccount *EncryptionManager::account() const { return d->olmAccount.data(); } diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 714f95fd..9d2c8138 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -9,12 +9,9 @@ #include #include -namespace QtOlm { -class Account; -} - namespace Quotient { class Connection; +class QOlmAccount; class EncryptionManager : public QObject { Q_OBJECT @@ -39,7 +36,7 @@ public: const QByteArray& senderKey); QByteArray olmAccountPickle(); - QtOlm::Account* account() const; + QOlmAccount* account() const; private: class Private; 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 12dd7bab004a0b85807347e9bac4ead2baf56bc5 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 28 Jan 2021 23:49:52 +0100 Subject: Fix test --- autotests/testolmsession.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 6535e4fe..72c54174 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -25,7 +25,7 @@ std::pair, std::unique_ptr> createSess const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey - if (preKey.type() != Message::PreKey) { + if (preKey.type() != QOlmMessage::PreKey) { throw "Wrong first message type received, can't create session"; } auto inbound = std::get>(accountB.createInboundSession(preKey)); @@ -42,8 +42,8 @@ void TestOlmSession::olmEncryptDecrypt() { const auto [inboundSession, outboundSession] = createSessionPair(); const auto encrypted = outboundSession->encrypt("Hello world!"); - if (encrypted.type() == Message::PreKey) { - Message m(encrypted); // clone + if (encrypted.type() == QOlmMessage::PreKey) { + QOlmMessage m(encrypted); // clone QVERIFY(std::get(inboundSession->matchesInboundSession(m))); } -- cgit v1.2.3 From 0769764249e10f2f6d1a84ac87e93b2fa3b6c61a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 29 Jan 2021 02:53:01 +0100 Subject: More porting to new API --- lib/crypto/qolmaccount.cpp | 10 +++++ lib/crypto/qolmaccount.h | 3 ++ lib/crypto/qolmsession.cpp | 21 +++++++++- lib/crypto/qolmsession.h | 9 ++++- lib/encryptionmanager.cpp | 95 ++++++++++++++++++++++++---------------------- 5 files changed, 90 insertions(+), 48 deletions(-) diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index fc0fc1cf..76b0a263 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -197,6 +197,16 @@ QByteArray QOlmAccount::signOneTimeKey(const QString &key) const return sign(j.toJson()); } +std::optional QOlmAccount::removeOneTimeKeys(const std::unique_ptr &session) const +{ + const auto error = olm_remove_one_time_keys(m_account, session->raw()); + + if (error == olm_error()) { + return lastError(m_account); + } + return std::nullopt; +} + OlmAccount *Quotient::QOlmAccount::data() { return m_account; diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index b33e3768..4398214a 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -68,6 +68,9 @@ public: SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + //! Remove the one time key used to create the supplied session. + [[nodiscard]] std::optional removeOneTimeKeys(const std::unique_ptr &session) const; + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param message An Olm pre-key message that was encrypted for this account. diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp index cfe21650..b901a440 100644 --- a/lib/crypto/qolmsession.cpp +++ b/lib/crypto/qolmsession.cpp @@ -213,7 +213,7 @@ bool QOlmSession::hasReceivedMessage() const return olm_session_has_received_message(m_session); } -std::variant QOlmSession::matchesInboundSession(QOlmMessage &preKeyMessage) +std::variant QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const { Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); QByteArray oneTimeKeyBuf(preKeyMessage.data()); @@ -231,6 +231,25 @@ std::variant QOlmSession::matchesInboundSession(QOlmMessage &pr return QOlmError::Unknown; } } +std::variant QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const +{ + const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + switch (error) { + case 0: + return false; + case 1: + return true; + default: + return QOlmError::Unknown; + } +} QOlmSession::QOlmSession(OlmSession *session) : m_session(session) diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h index 6e13801e..0fc59e9e 100644 --- a/lib/crypto/qolmsession.h +++ b/lib/crypto/qolmsession.h @@ -50,7 +50,10 @@ public: bool hasReceivedMessage() const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSession(QOlmMessage &preKeyMessage); + std::variant matchesInboundSession(const QOlmMessage &preKeyMessage) const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const; friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) { @@ -61,6 +64,10 @@ public: return *lhs < *rhs; } + OlmSession *raw() const + { + return m_session; + } QOlmSession(OlmSession* session); private: //! Helper function for creating new sessions and handling errors. diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 8081f788..449eb2a3 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -88,7 +88,7 @@ public: QHash targetOneTimeKeyCounts; // A map from senderKey to InboundSession - QMap sessions; // TODO: cache + QMap> sessions; // TODO: cache void updateDeviceKeys( const QHash>& deviceKeys) @@ -100,44 +100,45 @@ public: } } } - QString sessionDecrypt(QOlmMessage* message, const QString& senderKey) + QString sessionDecrypt(const QOlmMessage& message, const QString& senderKey) { - QString decrypted; - QList senderSessions = sessions.values(senderKey); // Try to decrypt message body using one of the known sessions for that // device bool sessionsPassed = false; // new e2ee TODO: - /* - for (auto senderSession : senderSessions) { - if (senderSession == senderSessions.last()) { + for (auto &senderSession : sessions) { + if (senderSession == sessions.last()) { sessionsPassed = true; } - try { - decrypted = senderSession->decrypt(message); + + const auto decryptedResult = senderSession->decrypt(message); + if (std::holds_alternative(decryptedResult)) { qCDebug(E2EE) << "Success decrypting Olm event using existing session" - << senderSession->id(); - break; - } catch (QOlmError* e) { - if (message->type() == QOlmMessage::PreKey) { - if (senderSession->matches(&message, senderKey)) { - // We had a matching session for a pre-key message, but - // it didn't work. This means something is wrong, so we - // fail now. - qCDebug(E2EE) - << "Error decrypting pre-key message with existing " - "Olm session" - << senderSession->id() << "reason:" << e->what(); - return QString(); + << senderSession->sessionId(); + return std::get(decryptedResult); + } else { + const auto error = std::get(decryptedResult); + if (message.type() == QOlmMessage::PreKey) { + const auto matches = senderSession->matchesInboundSessionFrom(senderKey, message); + if (auto hasMatch = std::get_if(&matches)) { + if (hasMatch) { + // We had a matching session for a pre-key message, but + // it didn't work. This means something is wrong, so we + // fail now. + qCDebug(E2EE) + << "Error decrypting pre-key message with existing " + "Olm session" + << senderSession->sessionId() << "reason:" << error; + return QString(); + } } } // Simply keep trying otherwise } } - */ - if (sessionsPassed || senderSessions.empty()) { - if (message->type() != QOlmMessage::PreKey) { + if (sessionsPassed || sessions.empty()) { + if (message.type() != QOlmMessage::PreKey) { // Not a pre-key message, we should have had a matching session if (!sessions.empty()) { qCDebug(E2EE) << "Error decrypting with existing sessions"; @@ -148,36 +149,39 @@ public: } // We have a pre-key message without any matching session, in this // case we should try to create one. - QOlmSession* newSession; qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; - QOlmMessage preKeyMessage = QOlmMessage(message->toCiphertext(),QOlmMessage::PreKey); + QOlmMessage preKeyMessage = QOlmMessage(message.toCiphertext(), QOlmMessage::PreKey); // new e2ee TODO: - /* - try { - newSession = new InboundSession(olmAccount.data(), - &preKeyMessage, - senderKey.toLatin1(), q); - } catch (OlmError* e) { + const auto sessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), preKeyMessage); + + if (const auto error = std::get_if(&sessionResult)) { qCDebug(E2EE) << "Error decrypting pre-key message when trying " "to establish a new session:" - << e->what(); + << error; return QString(); } - qCDebug(E2EE) << "Created new Olm session" << newSession->id(); - try { - decrypted = newSession->decrypt(message); - } catch (OlmError* e) { + + const auto newSession = std::get>(sessionResult); + + qCDebug(E2EE) << "Created new Olm session" << newSession->sessionId(); + + const auto decryptedResult = newSession->decrypt(message); + if (const auto error = std::get_if(&decryptedResult)) { qCDebug(E2EE) << "Error decrypting pre-key message with new session" - << e->what(); + << error; return QString(); } - olmAccount->removeOneTimeKeys(newSession); - */ - sessions.insert(senderKey, newSession); + if (auto error = olmAccount->removeOneTimeKeys(newSession)) { + qCDebug(E2EE) + << "Error removing one time keys" + << error.value(); + } + sessions.insert(senderKey, std::move(newSession)); + return std::get(decryptedResult); } - return decrypted; + return QString(); } }; @@ -330,11 +334,10 @@ QString EncryptionManager::sessionDecryptMessage( QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); if (type == 0) { QOlmMessage preKeyMessage = QOlmMessage(body, QOlmMessage::PreKey); - decrypted = d->sessionDecrypt(reinterpret_cast(&preKeyMessage), - senderKey); + decrypted = d->sessionDecrypt(preKeyMessage, senderKey); } else if (type == 1) { QOlmMessage message = QOlmMessage(body, QOlmMessage::PreKey); - decrypted = d->sessionDecrypt(&message, senderKey); + decrypted = d->sessionDecrypt(message, senderKey); } return decrypted; } -- cgit v1.2.3 From 10b89faeea9e385ea901d45418491cd91dff99b9 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 29 Jan 2021 20:23:42 +0100 Subject: More tests --- .ci/adjust-config.sh | 53 +++++++++++ Makefile | 31 +++++++ autotests/testolmaccount.cpp | 206 ++++++++++++++++++++++++++++++++++++++++++- autotests/testolmaccount.h | 4 + lib/connection.cpp | 51 +++++++---- lib/converters.cpp | 13 ++- lib/crypto/e2ee.h | 47 +++++++++- lib/crypto/qolmaccount.cpp | 36 +++++++- lib/crypto/qolmaccount.h | 8 +- lib/encryptionmanager.cpp | 12 +-- lib/networkaccessmanager.cpp | 6 +- 11 files changed, 435 insertions(+), 32 deletions(-) create mode 100755 .ci/adjust-config.sh create mode 100644 Makefile diff --git a/.ci/adjust-config.sh b/.ci/adjust-config.sh new file mode 100755 index 00000000..b2ca52b2 --- /dev/null +++ b/.ci/adjust-config.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +CMD="" + +$CMD perl -pi -w -e \ + 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g;' data/homeserver.yaml +$CMD perl -pi -w -e \ + 's/rc_message_burst_count.*/rc_message_burst_count: 10000/g;' data/homeserver.yaml + +( +cat <&1>/dev/null + +restart: stop-synapse synapse diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index a4dfd7b5..c764e023 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -4,7 +4,7 @@ #include "testolmaccount.h" #include "crypto/qolmaccount.h" -#include "csapi/definitions/device_keys.h" +#include "connection.h" #include "events/encryptedfile.h" using namespace Quotient; @@ -162,4 +162,208 @@ void TestOlmAccount::encryptedFile() QCOMPARE(file.key.keyOps.count(), 2); QCOMPARE(file.key.kty, "oct"); } + +void TestOlmAccount::uploadIdentityKey() +{ + auto conn = new Connection(); + conn->resolveServer("@alice:localhost:" + QString::number(443)); + connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { + conn->loginWithPassword("alice", "secret", "AlicePhone", ""); + connect(conn, &Connection::connected, this, [this, conn] { + auto olmAccount = conn->olmAccount(); + auto idKeys = olmAccount->identityKeys(); + + QVERIFY(idKeys.curve25519.size() > 10); + QVERIFY(idKeys.curve25519.size() > 10); + + + OneTimeKeys unused; + auto request = olmAccount->createUploadKeyRequest(unused); + connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 0); + }); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); + }); + connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, [=](QString error, const QString &) { + QFAIL("Login failed"); + }); + }); + + connect(conn, &Connection::resolveError, this, [=](QString error) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, this, [=] { + QFAIL("Network error: make sure synapse is running"); + }); + + QSignalSpy spy(conn, &Connection::loginFlowsChanged); + QSignalSpy spy2(conn, &Connection::connected); + QVERIFY(spy.wait(10000)); + QVERIFY(spy2.wait(10000)); + delete conn; +} + +void TestOlmAccount::uploadOneTimeKeys() +{ + auto conn = new Connection(); + conn->resolveServer("@alice:localhost:" + QString::number(443)); + connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { + conn->loginWithPassword("alice", "secret", "AlicePhone", ""); + connect(conn, &Connection::connected, this, [this, conn] { + auto olmAccount = conn->olmAccount(); + + auto nKeys = olmAccount->generateOneTimeKeys(5); + QCOMPARE(nKeys, 5); + + auto oneTimeKeys = olmAccount->oneTimeKeys(); + + QHash oneTimeKeysHash; + const auto curve = oneTimeKeys.curve25519(); + for (const auto &[keyId, key] : asKeyValueRange(curve)) { + oneTimeKeysHash["curve25519:"+keyId] = key; + } + auto request = new UploadKeysJob(none, oneTimeKeysHash); + connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["curve25519"], 5); + }); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); + }); + connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, [=](QString error, const QString &) { + QFAIL("Login failed"); + }); + }); + + connect(conn, &Connection::resolveError, this, [=](QString error) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, this, [=] { + QFAIL("Network error: make sure synapse is running"); + }); + + QSignalSpy spy(conn, &Connection::loginFlowsChanged); + QSignalSpy spy2(conn, &Connection::connected); + QVERIFY(spy.wait(10000)); + QVERIFY(spy2.wait(10000)); + delete conn; +} + +void TestOlmAccount::uploadSignedOneTimeKeys() +{ + auto conn = new Connection(); + conn->resolveServer("@alice:localhost:" + QString::number(443)); + connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { + conn->loginWithPassword("alice", "secret", "AlicePhone", ""); + connect(conn, &Connection::connected, this, [this, conn] { + auto olmAccount = conn->olmAccount(); + auto nKeys = olmAccount->generateOneTimeKeys(5); + QCOMPARE(nKeys, 5); + + auto oneTimeKeys = olmAccount->oneTimeKeys(); + QHash oneTimeKeysHash; + const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys); + for (const auto &[keyId, key] : asKeyValueRange(signedKey)) { + QVariant var; + var.setValue(key); + oneTimeKeysHash[keyId] = var; + } + auto request = new UploadKeysJob(none, oneTimeKeysHash); + connect(request, &BaseJob::result, this, [request, nKeys, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], nKeys); + }); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); + }); + connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, [=](QString error, const QString &) { + QFAIL("Login failed"); + }); + }); + + connect(conn, &Connection::resolveError, this, [=](QString error) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, this, [=] { + QFAIL("Network error: make sure synapse is running"); + }); + + QSignalSpy spy(conn, &Connection::loginFlowsChanged); + QSignalSpy spy2(conn, &Connection::connected); + QVERIFY(spy.wait(10000)); + QVERIFY(spy2.wait(10000)); + delete conn; +} + +void TestOlmAccount::uploadKeys() +{ + auto conn = new Connection(); + conn->resolveServer("@alice:localhost:" + QString::number(443)); + connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { + conn->loginWithPassword("alice", "secret", "AlicePhone", ""); + connect(conn, &Connection::connected, this, [this, conn] { + auto olmAccount = conn->olmAccount(); + auto idks = olmAccount->identityKeys(); + olmAccount->generateOneTimeKeys(1); + auto otks = olmAccount->oneTimeKeys(); + auto request = olmAccount->createUploadKeyRequest(otks); + connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], 1); + }); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); + }); + connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, [=](QString error, const QString &) { + QFAIL("Login failed"); + }); + }); + + connect(conn, &Connection::resolveError, this, [=](QString error) { + QFAIL("Network error: make sure synapse is running"); + }); + connect(conn, &Connection::loginError, this, [=] { + QFAIL("Network error: make sure synapse is running"); + }); + + QSignalSpy spy(conn, &Connection::loginFlowsChanged); + QSignalSpy spy2(conn, &Connection::connected); + QVERIFY(spy.wait(10000)); + QVERIFY(spy2.wait(10000)); + delete conn; +} + QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 4e270730..41298957 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -16,4 +16,8 @@ private Q_SLOTS: //void removeOneTimeKeys(); void deviceKeys(); void encryptedFile(); + void uploadIdentityKey(); + void uploadOneTimeKeys(); + void uploadSignedOneTimeKeys(); + void uploadKeys(); }; diff --git a/lib/connection.cpp b/lib/connection.cpp index f96eeb71..704bc1b4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -108,7 +108,8 @@ public: QVector loginFlows; #ifdef Quotient_E2EE_ENABLED - QScopedPointer encryptionManager; + std::unique_ptr olmAccount; + //QScopedPointer encryptionManager; #endif // Quotient_E2EE_ENABLED QPointer resolverJob = nullptr; @@ -183,6 +184,9 @@ public: EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) { + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; + /* #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; @@ -242,6 +246,7 @@ public: return std::move(decryptedEvent); #endif // Quotient_E2EE_ENABLED +*/ } }; @@ -420,8 +425,8 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED - encryptionManager->uploadIdentityKeys(q); - encryptionManager->uploadOneTimeKeys(q); + //encryptionManager->uploadIdentityKeys(q); + //encryptionManager->uploadOneTimeKeys(q); #endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { @@ -442,11 +447,19 @@ void Connection::Private::completeSetup(const QString& mxId) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED AccountSettings accountSettings(data->userId()); - encryptionManager.reset( - new EncryptionManager(accountSettings.encryptionAccountPickle())); + + // init olmAccount + olmAccount = std::make_unique(data->userId(), data->deviceId()); + if (accountSettings.encryptionAccountPickle().isEmpty()) { - accountSettings.setEncryptionAccountPickle( - encryptionManager->olmAccountPickle()); + // create new account and save unpickle data + olmAccount->createNewAccount(); + accountSettings.setEncryptionAccountPickle(std::get(olmAccount->pickle(Unencrypted{}))); + // TODO handle pickle errors + } else { + // account already existing + auto pickle = accountSettings.encryptionAccountPickle(); + olmAccount->unpickle(pickle, Unencrypted{}); } #endif // Quotient_E2EE_ENABLED emit q->stateChanged(); @@ -608,16 +621,16 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED // handling device_one_time_keys_count - if (!d->encryptionManager) - { - qCDebug(E2EE) << "Encryption manager is not there yet, updating " - "one-time key counts will be skipped"; - return; - } - if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); - !deviceOneTimeKeysCount.isEmpty()) - d->encryptionManager->updateOneTimeKeyCounts(this, - deviceOneTimeKeysCount); + //if (!d->encryptionManager) + //{ + // qCDebug(E2EE) << "Encryption manager is not there yet, updating " + // "one-time key counts will be skipped"; + // return; + //} + //if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); + // !deviceOneTimeKeysCount.isEmpty()) + // d->encryptionManager->updateOneTimeKeyCounts(this, + // deviceOneTimeKeysCount); #endif // Quotient_E2EE_ENABLED } @@ -745,6 +758,7 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { +/* #ifdef Quotient_E2EE_ENABLED // handling m.room_key to-device encrypted event visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) { @@ -775,6 +789,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) }); }); #endif +*/ } void Connection::stopSync() @@ -1228,7 +1243,7 @@ bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } #ifdef Quotient_E2EE_ENABLED QOlmAccount *Connection::olmAccount() const { - return d->encryptionManager->account(); + return d->olmAccount.get(); //d->encryptionManager->account(); } #endif // Quotient_E2EE_ENABLED diff --git a/lib/converters.cpp b/lib/converters.cpp index 444ca4f6..e6dcd854 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -3,15 +3,26 @@ #include "converters.h" -#include +#include +#include "crypto/e2ee.h" QJsonValue Quotient::JsonConverter::dump(const QVariant& v) { + if (v.canConvert()) { + return toJson(v.value()); + } return QJsonValue::fromVariant(v); } QVariant Quotient::JsonConverter::load(const QJsonValue& jv) { + if (jv.isObject()) { + QJsonObject obj = jv.toObject(); + if (obj.contains("key") && obj.contains("signatures")) { + SignedOneTimeKey signedOneTimeKeys; + signedOneTimeKeys.key = obj["key"].toString(); + } + } return jv.toVariant(); } diff --git a/lib/crypto/e2ee.h b/lib/crypto/e2ee.h index 73dd7f65..2d280185 100644 --- a/lib/crypto/e2ee.h +++ b/lib/crypto/e2ee.h @@ -7,10 +7,13 @@ #include #include +#include "converters.h" #include #include +#include #include +#include #include "util.h" @@ -68,16 +71,56 @@ struct OneTimeKeys }; //! Struct representing the signed one-time keys. -struct SignedOneTimeKey +class SignedOneTimeKey { +public: + SignedOneTimeKey() = default; + SignedOneTimeKey(const SignedOneTimeKey &) = default; + SignedOneTimeKey &operator=(const SignedOneTimeKey &) = default; //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. QString key; //! Required. Signatures of the key object. //! The signature is calculated using the process described at Signing JSON. - QMap> signatures; + QHash> signatures; +}; + + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, + SignedOneTimeKey& result) + { + fromJson(jo.value("key"_ls), result.key); + fromJson(jo.value("signatures"_ls), result.signatures); + } + + static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) + { + addParam<>(jo, QStringLiteral("key"), result.key); + addParam<>(jo, QStringLiteral("signatures"), result.signatures); + } }; bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); +template +class asKeyValueRange +{ +public: + asKeyValueRange(T &data) + : m_data{data} + { + } + + auto begin() { return m_data.keyValueBegin(); } + + auto end() { return m_data.keyValueEnd(); } + +private: + T &m_data; +}; + } // namespace Quotient + +Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 76b0a263..fb91c906 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -4,6 +4,8 @@ #ifdef Quotient_E2EE_ENABLED #include "qolmaccount.h" +#include "connection.h" +#include "csapi/keys.h" #include "crypto/qolmutils.h" #include #include @@ -138,7 +140,7 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const return olm_account_max_number_of_one_time_keys(m_account); } -void QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const { const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); QByteArray randomBuffer = getRandom(randomLen); @@ -147,6 +149,7 @@ void QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const if (error == olm_error()) { throw lastError(m_account); } + return error; } OneTimeKeys QOlmAccount::oneTimeKeys() const @@ -212,6 +215,37 @@ OlmAccount *Quotient::QOlmAccount::data() return m_account; } +UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) +{ + + DeviceKeys deviceKeys; + deviceKeys.userId = m_userId; + deviceKeys.deviceId = m_deviceId; + deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}; + + const auto idKeys = identityKeys(); + deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519; + deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519; + + const auto sign = signIdentityKeys(); + deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; + + if (oneTimeKeys.curve25519().isEmpty()) { + return new UploadKeysJob(deviceKeys); + } + + // Sign & append the one time keys. + auto temp = signOneTimeKeys(oneTimeKeys); + QHash oneTimeKeysSigned; + for (const auto &[keyId, key] : asKeyValueRange(temp)) { + QVariant keyVar; + keyVar.setValue(key); + oneTimeKeysSigned[keyId] = keyVar; + } + + return new UploadKeysJob(deviceKeys, oneTimeKeysSigned); +} + std::variant, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index 4398214a..d61c8748 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -4,6 +4,7 @@ #pragma once #ifdef Quotient_E2EE_ENABLED +#include "csapi/keys.h" #include "crypto/e2ee.h" #include "crypto/qolmerrors.h" #include "crypto/qolmmessage.h" @@ -15,6 +16,7 @@ struct OlmAccount; namespace Quotient { class QOlmSession; +class Connection; //! An olm account manages all cryptographic keys used on a device. //! \code{.cpp} @@ -55,7 +57,7 @@ public: size_t maxNumberOfOneTimeKeys() const; //! Generates the supplied number of one time keys. - void generateOneTimeKeys(size_t numberOfKeys) const; + size_t generateOneTimeKeys(size_t numberOfKeys) const; //! Gets the OlmAccount's one time keys formatted as JSON. OneTimeKeys oneTimeKeys() const; @@ -68,6 +70,8 @@ public: SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); + //! Remove the one time key used to create the supplied session. [[nodiscard]] std::optional removeOneTimeKeys(const std::unique_ptr &session) const; @@ -90,7 +94,7 @@ public: QOlmAccount(OlmAccount *account); OlmAccount *data(); private: - OlmAccount *m_account = nullptr; + OlmAccount *m_account = nullptr; // owning QString m_userId; QString m_deviceId; }; diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 449eb2a3..c8dc6bdd 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -104,7 +104,7 @@ public: { // Try to decrypt message body using one of the known sessions for that // device - bool sessionsPassed = false; + /*bool sessionsPassed = false; // new e2ee TODO: for (auto &senderSession : sessions) { if (senderSession == sessions.last()) { @@ -152,7 +152,7 @@ public: qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; QOlmMessage preKeyMessage = QOlmMessage(message.toCiphertext(), QOlmMessage::PreKey); // new e2ee TODO: - const auto sessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), preKeyMessage); + //const auto sessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), preKeyMessage); if (const auto error = std::get_if(&sessionResult)) { qCDebug(E2EE) << "Error decrypting pre-key message when trying " @@ -161,7 +161,7 @@ public: return QString(); } - const auto newSession = std::get>(sessionResult); + const auto newSession = std::get>(olmAccount->createInboundSessionFrom(senderKey.toUtf8(), preKeyMessage)); qCDebug(E2EE) << "Created new Olm session" << newSession->sessionId(); @@ -178,9 +178,9 @@ public: << "Error removing one time keys" << error.value(); } - sessions.insert(senderKey, std::move(newSession)); - return std::get(decryptedResult); - } + //sessions.insert(senderKey, std::move(newSession)); TODO + //return std::get(decryptedResult); + }*/ return QString(); } }; diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 57618329..293538ee 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -40,7 +40,11 @@ public: NetworkAccessManager::NetworkAccessManager(QObject* parent) : QNetworkAccessManager(parent), d(std::make_unique(this)) -{} +{ + connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList &errors) { + reply->ignoreSslErrors(); + }); +} QList NetworkAccessManager::ignoredSslErrors() const { -- cgit v1.2.3 From f9f7d130e5768d0f69edc8900d37f540b61fa974 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 30 Jan 2021 00:21:10 +0100 Subject: Key verification --- CMakeLists.txt | 1 + autotests/testolmaccount.cpp | 138 +++++++++++++++++++++++++++++++------------ autotests/testolmaccount.h | 1 + lib/crypto/qolmaccount.cpp | 42 +++++++++++++ lib/crypto/qolmaccount.h | 9 +++ lib/crypto/qolmutility.cpp | 58 ++++++++++++++++++ lib/crypto/qolmutility.h | 48 +++++++++++++++ 7 files changed, 258 insertions(+), 39 deletions(-) create mode 100644 lib/crypto/qolmutility.cpp create mode 100644 lib/crypto/qolmutility.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f62af68..fb07fa22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,6 +163,7 @@ list(APPEND lib_SRCS lib/crypto/qolminboundsession.cpp lib/crypto/qolmoutboundsession.cpp lib/crypto/qolmutils.cpp + lib/crypto/qolmutility.cpp lib/crypto/qolmerrors.cpp lib/crypto/qolmsession.cpp lib/crypto/qolmmessage.cpp diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index c764e023..ce51b9ec 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -244,19 +244,6 @@ void TestOlmAccount::uploadOneTimeKeys() QSignalSpy spy3(request, &BaseJob::result); QVERIFY(spy3.wait(10000)); }); - connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, [=](QString error, const QString &) { - QFAIL("Login failed"); - }); - }); - - connect(conn, &Connection::resolveError, this, [=](QString error) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, this, [=] { - QFAIL("Network error: make sure synapse is running"); }); QSignalSpy spy(conn, &Connection::loginFlowsChanged); @@ -298,19 +285,6 @@ void TestOlmAccount::uploadSignedOneTimeKeys() QSignalSpy spy3(request, &BaseJob::result); QVERIFY(spy3.wait(10000)); }); - connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, [=](QString error, const QString &) { - QFAIL("Login failed"); - }); - }); - - connect(conn, &Connection::resolveError, this, [=](QString error) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, this, [=] { - QFAIL("Network error: make sure synapse is running"); }); QSignalSpy spy(conn, &Connection::loginFlowsChanged); @@ -344,19 +318,6 @@ void TestOlmAccount::uploadKeys() QSignalSpy spy3(request, &BaseJob::result); QVERIFY(spy3.wait(10000)); }); - connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, [=](QString error, const QString &) { - QFAIL("Login failed"); - }); - }); - - connect(conn, &Connection::resolveError, this, [=](QString error) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, this, [=] { - QFAIL("Network error: make sure synapse is running"); }); QSignalSpy spy(conn, &Connection::loginFlowsChanged); @@ -366,4 +327,103 @@ void TestOlmAccount::uploadKeys() delete conn; } +inline void sleep() +{ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + + +void TestOlmAccount::claimKeys() +{ + auto alice = new Connection(); + alice->resolveServer("@alice:localhost:" + QString::number(443)); + connect(alice, &Connection::loginFlowsChanged, this, [this, alice]() { + alice->loginWithPassword("alice", "secret", "AlicePhone", ""); + connect(alice, &Connection::connected, this, [this, alice] { + qDebug() << "alice->accessToken()" << alice->accessToken(); + QVERIFY(!alice->accessToken().isEmpty()); + }); + }); + + QSignalSpy spy(alice, &Connection::loginFlowsChanged); + QSignalSpy spy2(alice, &Connection::connected); + QVERIFY(spy.wait(10000)); + QVERIFY(spy2.wait(10000)); + + auto bob = new Connection(); + bob->resolveServer("@bob:localhost:" + QString::number(443)); + connect(bob, &Connection::loginFlowsChanged, this, [this, bob]() { + bob->loginWithPassword("bob", "secret", "BobPhone", ""); + connect(bob, &Connection::connected, this, [this, bob] { + qDebug() << "bob->accessToken()" << bob->accessToken(); + QVERIFY(!bob->accessToken().isEmpty()); + }); + }); + + QSignalSpy spy3(bob, &Connection::loginFlowsChanged); + QSignalSpy spy4(bob, &Connection::connected); + QVERIFY(spy3.wait(10000)); + QVERIFY(spy4.wait(10000)); + + // Bob uploads his keys. + auto *bobOlm = bob->olmAccount(); + bobOlm->generateOneTimeKeys(1); + auto request = bobOlm->createUploadKeyRequest(bobOlm->oneTimeKeys()); + + connect(request, &BaseJob::result, this, [request, bob](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], 1); + }); + bob->run(request); + + QSignalSpy requestSpy(request, &BaseJob::result); + QVERIFY(requestSpy.wait(10000)); + + // Alice retrieves bob's keys & claims one signed one-time key. + auto *aliceOlm = alice->olmAccount(); + QHash deviceKeys; + deviceKeys[bob->userId()] = QStringList(); + auto job = alice->callApi(deviceKeys); + connect(job, &BaseJob::result, this, [bob, alice, aliceOlm, job, this] { + auto bobDevices = job->deviceKeys()[bob->userId()]; + QVERIFY(bobDevices.size() > 0); + + auto devices = {bob->deviceId()}; + + // Retrieve the identity key for the current device. + auto bobEd25519 = + bobDevices[bob->deviceId()].keys["ed25519:" + bob->deviceId()]; + + const auto currentDevice = bobDevices[bob->deviceId()]; + + // Verify signature. + QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), bob->userId())); + + QHash> oneTimeKeys; + for (const auto &d : devices) { + oneTimeKeys[bob->userId()] = QHash(); + oneTimeKeys[bob->userId()][d] = SignedCurve25519Key; + } + auto job = alice->callApi(oneTimeKeys); + connect(job, &BaseJob::result, this, [aliceOlm, bob, bobEd25519, job] { + const auto userId = bob->userId(); + const auto deviceId = bob->deviceId(); + + // The device exists. + QCOMPARE(job->oneTimeKeys().size(), 1); + QCOMPARE(job->oneTimeKeys()[userId].size(), 1); + + // The key is the one bob sent. + auto oneTimeKey = job->oneTimeKeys()[userId][deviceId]; + QVERIFY(oneTimeKey.canConvert()); + + //auto algo = oneTimeKey.begin().key(); + //auto contents = oneTimeKey.begin().value(); + }); + }); + delete bob; + delete alice; +} + QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 41298957..8b2d2e09 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -20,4 +20,5 @@ private Q_SLOTS: void uploadOneTimeKeys(); void uploadSignedOneTimeKeys(); void uploadKeys(); + void claimKeys(); }; diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index fb91c906..24fd87f2 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -7,6 +7,7 @@ #include "connection.h" #include "csapi/keys.h" #include "crypto/qolmutils.h" +#include "crypto/qolmutility.h" #include #include #include @@ -263,4 +264,45 @@ std::variant, QOlmError> QOlmAccount::createOutboun return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); } +bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, + const QString &deviceId, + const QString &userId) +{ + const auto signKeyId = "ed25519:" + deviceId; + const auto signingKey = deviceKeys.keys[signKeyId]; + const auto signature = deviceKeys.signatures[userId][signKeyId]; + + if (signature.isEmpty()) { + return false; + } + + return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); +} + +bool Quotient::ed25519VerifySignature(QString signingKey, + QJsonObject obj, + QString signature) +{ + if (signature.isEmpty()) { + return false; + } + + obj.remove("unsigned"); + obj.remove("signatures"); + + QJsonDocument doc; + doc.setObject(obj); + auto canonicalJson = doc.toJson(); + + QByteArray signingKeyBuf = signingKey.toUtf8(); + QOlmUtility utility; + auto signatureBuf = signature.toUtf8(); + auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); + if (std::holds_alternative(result)) { + return false; + } + + return std::get(result); +} + #endif diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index d61c8748..09ef623a 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -99,6 +99,15 @@ private: QString m_deviceId; }; +bool verifyIdentitySignature(const DeviceKeys &deviceKeys, + const QString &deviceId, + const QString &userId); + +//! checks if the signature is signed by the signing_key +bool ed25519VerifySignature(QString signingKey, + QJsonObject obj, + QString signature); + } // namespace Quotient #endif diff --git a/lib/crypto/qolmutility.cpp b/lib/crypto/qolmutility.cpp new file mode 100644 index 00000000..3c6a14c7 --- /dev/null +++ b/lib/crypto/qolmutility.cpp @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifdef Quotient_E2EE_ENABLED +#include "crypto/qolmutility.h" +#include "olm/olm.h" + +using namespace Quotient; + +// Convert olm error to enum +QOlmError lastError(OlmUtility *utility) { + const std::string error_raw = olm_utility_last_error(utility); + + return fromString(error_raw); +} + +QOlmUtility::QOlmUtility() +{ + auto utility = new uint8_t[olm_utility_size()]; + m_utility = olm_utility(utility); +} + +QOlmUtility::~QOlmUtility() +{ + olm_clear_utility(m_utility); + delete[](reinterpret_cast(m_utility)); +} + +QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const +{ + const auto outputLen = olm_sha256_length(m_utility); + QByteArray outputBuf(outputLen, '0'); + olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), + outputBuf.data(), outputBuf.length()); + + return QString::fromUtf8(outputBuf); +} + +QString QOlmUtility::sha256Utf8Msg(const QString &message) const +{ + return sha256Bytes(message.toUtf8()); +} + +std::variant QOlmUtility::ed25519Verify(const QByteArray &key, + const QByteArray &message, QByteArray &signature) +{ + const auto error = olm_ed25519_verify(m_utility, key.data(), key.length(), + message.data(), message.length(), signature.data(), signature.length()); + + if (error == olm_error()) { + return lastError(m_utility); + } + return error == 0; +} + + +#endif diff --git a/lib/crypto/qolmutility.h b/lib/crypto/qolmutility.h new file mode 100644 index 00000000..16c330eb --- /dev/null +++ b/lib/crypto/qolmutility.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef Quotient_E2EE_ENABLED +#include +#include +#include "crypto/qolmerrors.h" + +struct OlmUtility; + +namespace Quotient { + +class QOlmSession; +class Connection; + +//! Allows you to make use of crytographic hashing via SHA-2 and +//! verifying ed25519 signatures. +class QOlmUtility +{ +public: + QOlmUtility(); + ~QOlmUtility(); + + //! Returns a sha256 of the supplied byte slice. + QString sha256Bytes(const QByteArray &inputBuf) const; + + //! Convenience function that converts the UTF-8 message + //! to bytes and then calls `sha256_bytes()`, returning its output. + QString sha256Utf8Msg(const QString &message) const; + + //! Verify a ed25519 signature. + //! \param any QByteArray The public part of the ed25519 key that signed the message. + //! \param message QByteArray The message that was signed. + //! \param signature QByteArray The signature of the message. + std::variant ed25519Verify(const QByteArray &key, + const QByteArray &message, QByteArray &signature); + + +private: + OlmUtility *m_utility; + +}; +} + +#endif -- cgit v1.2.3 From 4593856411a2a8e4b82333abd5684b253daab47c Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 30 Jan 2021 01:26:30 +0100 Subject: Add more test and use macro to remove duplicated code --- autotests/testolmaccount.cpp | 343 +++++++++++++++++++++---------------------- autotests/testolmaccount.h | 7 + lib/crypto/qolmaccount.cpp | 5 +- 3 files changed, 175 insertions(+), 180 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index ce51b9ec..5cb88a99 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -163,207 +163,130 @@ void TestOlmAccount::encryptedFile() QCOMPARE(file.key.kty, "oct"); } +#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ + auto VAR = std::make_shared(); \ + (VAR) ->resolveServer("@alice:localhost:" + QString::number(443)); \ + connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [this, VAR ] () { \ + (VAR) ->loginWithPassword( (USERNAME) , SECRET , DEVICE_NAME , ""); \ + }); \ + connect( (VAR) .get(), &Connection::networkError, [=](QString error, const QString &, int, int) { \ + QFAIL("Network error: make sure synapse is running"); \ + }); \ + connect( (VAR) .get(), &Connection::loginError, [=](QString error, const QString &) { \ + QFAIL("Login failed"); \ + }); \ + QSignalSpy spy ## VAR ((VAR).get(), &Connection::loginFlowsChanged); \ + QSignalSpy spy2 ## VAR ((VAR).get(), &Connection::connected); \ + QVERIFY(spy ## VAR .wait(10000)); \ + QVERIFY(spy2 ## VAR .wait(10000)); + void TestOlmAccount::uploadIdentityKey() { - auto conn = new Connection(); - conn->resolveServer("@alice:localhost:" + QString::number(443)); - connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { - conn->loginWithPassword("alice", "secret", "AlicePhone", ""); - connect(conn, &Connection::connected, this, [this, conn] { - auto olmAccount = conn->olmAccount(); - auto idKeys = olmAccount->identityKeys(); - - QVERIFY(idKeys.curve25519.size() > 10); - QVERIFY(idKeys.curve25519.size() > 10); - - - OneTimeKeys unused; - auto request = olmAccount->createUploadKeyRequest(unused); - connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 0); - }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); - conn->run(request); - QSignalSpy spy3(request, &BaseJob::result); - QVERIFY(spy3.wait(10000)); - }); - connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { - QFAIL("Network error: make sure synapse is running"); - }); - connect(conn, &Connection::loginError, [=](QString error, const QString &) { - QFAIL("Login failed"); - }); - }); + CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + + auto olmAccount = conn->olmAccount(); + auto idKeys = olmAccount->identityKeys(); - connect(conn, &Connection::resolveError, this, [=](QString error) { - QFAIL("Network error: make sure synapse is running"); + QVERIFY(idKeys.curve25519.size() > 10); + QVERIFY(idKeys.curve25519.size() > 10); + + OneTimeKeys unused; + auto request = olmAccount->createUploadKeyRequest(unused); + connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 0); }); - connect(conn, &Connection::loginError, this, [=] { - QFAIL("Network error: make sure synapse is running"); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); }); - - QSignalSpy spy(conn, &Connection::loginFlowsChanged); - QSignalSpy spy2(conn, &Connection::connected); - QVERIFY(spy.wait(10000)); - QVERIFY(spy2.wait(10000)); - delete conn; + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); } void TestOlmAccount::uploadOneTimeKeys() { - auto conn = new Connection(); - conn->resolveServer("@alice:localhost:" + QString::number(443)); - connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { - conn->loginWithPassword("alice", "secret", "AlicePhone", ""); - connect(conn, &Connection::connected, this, [this, conn] { - auto olmAccount = conn->olmAccount(); - - auto nKeys = olmAccount->generateOneTimeKeys(5); - QCOMPARE(nKeys, 5); - - auto oneTimeKeys = olmAccount->oneTimeKeys(); - - QHash oneTimeKeysHash; - const auto curve = oneTimeKeys.curve25519(); - for (const auto &[keyId, key] : asKeyValueRange(curve)) { - oneTimeKeysHash["curve25519:"+keyId] = key; - } - auto request = new UploadKeysJob(none, oneTimeKeysHash); - connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["curve25519"], 5); - }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); - conn->run(request); - QSignalSpy spy3(request, &BaseJob::result); - QVERIFY(spy3.wait(10000)); - }); - }); + CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + auto olmAccount = conn->olmAccount(); - QSignalSpy spy(conn, &Connection::loginFlowsChanged); - QSignalSpy spy2(conn, &Connection::connected); - QVERIFY(spy.wait(10000)); - QVERIFY(spy2.wait(10000)); - delete conn; + auto nKeys = olmAccount->generateOneTimeKeys(5); + QCOMPARE(nKeys, 5); + + auto oneTimeKeys = olmAccount->oneTimeKeys(); + + QHash oneTimeKeysHash; + const auto curve = oneTimeKeys.curve25519(); + for (const auto &[keyId, key] : asKeyValueRange(curve)) { + oneTimeKeysHash["curve25519:"+keyId] = key; + } + auto request = new UploadKeysJob(none, oneTimeKeysHash); + connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["curve25519"], 5); + }); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); } void TestOlmAccount::uploadSignedOneTimeKeys() { - auto conn = new Connection(); - conn->resolveServer("@alice:localhost:" + QString::number(443)); - connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { - conn->loginWithPassword("alice", "secret", "AlicePhone", ""); - connect(conn, &Connection::connected, this, [this, conn] { - auto olmAccount = conn->olmAccount(); - auto nKeys = olmAccount->generateOneTimeKeys(5); - QCOMPARE(nKeys, 5); - - auto oneTimeKeys = olmAccount->oneTimeKeys(); - QHash oneTimeKeysHash; - const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys); - for (const auto &[keyId, key] : asKeyValueRange(signedKey)) { - QVariant var; - var.setValue(key); - oneTimeKeysHash[keyId] = var; - } - auto request = new UploadKeysJob(none, oneTimeKeysHash); - connect(request, &BaseJob::result, this, [request, nKeys, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], nKeys); - }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); - conn->run(request); - QSignalSpy spy3(request, &BaseJob::result); - QVERIFY(spy3.wait(10000)); - }); + CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + auto olmAccount = conn->olmAccount(); + auto nKeys = olmAccount->generateOneTimeKeys(5); + QCOMPARE(nKeys, 5); + + auto oneTimeKeys = olmAccount->oneTimeKeys(); + QHash oneTimeKeysHash; + const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys); + for (const auto &[keyId, key] : asKeyValueRange(signedKey)) { + QVariant var; + var.setValue(key); + oneTimeKeysHash[keyId] = var; + } + auto request = new UploadKeysJob(none, oneTimeKeysHash); + connect(request, &BaseJob::result, this, [request, nKeys, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], nKeys); }); - - QSignalSpy spy(conn, &Connection::loginFlowsChanged); - QSignalSpy spy2(conn, &Connection::connected); - QVERIFY(spy.wait(10000)); - QVERIFY(spy2.wait(10000)); - delete conn; + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); } void TestOlmAccount::uploadKeys() { - auto conn = new Connection(); - conn->resolveServer("@alice:localhost:" + QString::number(443)); - connect(conn, &Connection::loginFlowsChanged, this, [this, conn]() { - conn->loginWithPassword("alice", "secret", "AlicePhone", ""); - connect(conn, &Connection::connected, this, [this, conn] { - auto olmAccount = conn->olmAccount(); - auto idks = olmAccount->identityKeys(); - olmAccount->generateOneTimeKeys(1); - auto otks = olmAccount->oneTimeKeys(); - auto request = olmAccount->createUploadKeyRequest(otks); - connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], 1); - }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); - conn->run(request); - QSignalSpy spy3(request, &BaseJob::result); - QVERIFY(spy3.wait(10000)); - }); + CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + auto olmAccount = conn->olmAccount(); + auto idks = olmAccount->identityKeys(); + olmAccount->generateOneTimeKeys(1); + auto otks = olmAccount->oneTimeKeys(); + auto request = olmAccount->createUploadKeyRequest(otks); + connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { + auto job2 = static_cast(job); + QCOMPARE(job2->oneTimeKeyCounts().size(), 1); + QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], 1); }); - - QSignalSpy spy(conn, &Connection::loginFlowsChanged); - QSignalSpy spy2(conn, &Connection::connected); - QVERIFY(spy.wait(10000)); - QVERIFY(spy2.wait(10000)); - delete conn; -} - -inline void sleep() -{ - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + connect(request, &BaseJob::failure, this, [] { + QFAIL("upload failed"); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); } - void TestOlmAccount::claimKeys() { - auto alice = new Connection(); - alice->resolveServer("@alice:localhost:" + QString::number(443)); - connect(alice, &Connection::loginFlowsChanged, this, [this, alice]() { - alice->loginWithPassword("alice", "secret", "AlicePhone", ""); - connect(alice, &Connection::connected, this, [this, alice] { - qDebug() << "alice->accessToken()" << alice->accessToken(); - QVERIFY(!alice->accessToken().isEmpty()); - }); - }); - - QSignalSpy spy(alice, &Connection::loginFlowsChanged); - QSignalSpy spy2(alice, &Connection::connected); - QVERIFY(spy.wait(10000)); - QVERIFY(spy2.wait(10000)); - - auto bob = new Connection(); - bob->resolveServer("@bob:localhost:" + QString::number(443)); - connect(bob, &Connection::loginFlowsChanged, this, [this, bob]() { - bob->loginWithPassword("bob", "secret", "BobPhone", ""); - connect(bob, &Connection::connected, this, [this, bob] { - qDebug() << "bob->accessToken()" << bob->accessToken(); - QVERIFY(!bob->accessToken().isEmpty()); - }); - }); - - QSignalSpy spy3(bob, &Connection::loginFlowsChanged); - QSignalSpy spy4(bob, &Connection::connected); - QVERIFY(spy3.wait(10000)); - QVERIFY(spy4.wait(10000)); + CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "alice", "secret", "AlicePhone") // Bob uploads his keys. auto *bobOlm = bob->olmAccount(); @@ -422,8 +345,70 @@ void TestOlmAccount::claimKeys() //auto contents = oneTimeKey.begin().value(); }); }); - delete bob; - delete alice; } +void TestOlmAccount::claimMultipleKeys() +{ + // Login with alice multiple times + CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(alice1, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(alice2, "alice", "secret", "AlicePhone") + + auto olm = alice->olmAccount(); + olm->generateOneTimeKeys(10); + auto res = olm->createUploadKeyRequest(olm->oneTimeKeys()); + alice->run(res); + connect(res, &BaseJob::result, this, [res] { + QCOMPARE(res->oneTimeKeyCounts().size(), 1); + QCOMPARE(res->oneTimeKeyCounts()["signed_curve25519"], 10); + }); + QSignalSpy spy(res, &BaseJob::result); + + auto olm1 = alice1->olmAccount(); + olm1->generateOneTimeKeys(10); + auto res1 = olm1->createUploadKeyRequest(olm1->oneTimeKeys()); + alice1->run(res1); + connect(res1, &BaseJob::result, this, [res1] { + QCOMPARE(res1->oneTimeKeyCounts().size(), 1); + QCOMPARE(res1->oneTimeKeyCounts()["signed_curve25519"], 10); + }); + QSignalSpy spy1(res1, &BaseJob::result); + + auto olm2 = alice2->olmAccount(); + olm2->generateOneTimeKeys(10); + auto res2 = olm2->createUploadKeyRequest(olm2->oneTimeKeys()); + alice2->run(res2); + connect(res2, &BaseJob::result, this, [res2] { + QCOMPARE(res2->oneTimeKeyCounts().size(), 1); + QCOMPARE(res2->oneTimeKeyCounts()["signed_curve25519"], 10); + }); + QSignalSpy spy2(res2, &BaseJob::result); + + QVERIFY(spy.wait(10000)); + QVERIFY(spy1.wait(10000)); + QVERIFY(spy2.wait(10000)); + + // Bob will claim all keys from alice + CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + + QStringList devices_; + devices_ << alice->deviceId() + << alice1->deviceId() + << alice2->deviceId(); + + QHash> oneTimeKeys; + for (const auto &d : devices_) { + oneTimeKeys[alice->userId()] = QHash(); + oneTimeKeys[alice->userId()][d] = SignedCurve25519Key; + } + auto job = bob->callApi(oneTimeKeys); + connect(job, &BaseJob::result, this, [bob, job] { + const auto userId = bob->userId(); + const auto deviceId = bob->deviceId(); + + // The device exists. + QCOMPARE(job->oneTimeKeys().size(), 1); + QCOMPARE(job->oneTimeKeys()[userId].size(), 3); + }); +} QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 8b2d2e09..b6b9cae4 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -3,11 +3,17 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include +#include + +namespace Quotient { + class Connection; +} class TestOlmAccount : public QObject { Q_OBJECT + private Q_SLOTS: void pickleUnpickedTest(); void identityKeysValid(); @@ -21,4 +27,5 @@ private Q_SLOTS: void uploadSignedOneTimeKeys(); void uploadKeys(); void claimKeys(); + void claimMultipleKeys(); }; diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 24fd87f2..750d7318 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -272,6 +272,7 @@ bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, const auto signingKey = deviceKeys.keys[signKeyId]; const auto signature = deviceKeys.signatures[userId][signKeyId]; + if (signature.isEmpty()) { return false; } @@ -292,7 +293,9 @@ bool Quotient::ed25519VerifySignature(QString signingKey, QJsonDocument doc; doc.setObject(obj); - auto canonicalJson = doc.toJson(); + auto canonicalJson = doc.toJson(QJsonDocument::Compact); + + qDebug() << canonicalJson; QByteArray signingKeyBuf = signingKey.toUtf8(); QOlmUtility utility; -- cgit v1.2.3 From fe9b2f918753d40d93f8aecf182485e75d4b75bb Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 31 Jan 2021 01:18:35 +0100 Subject: More test but still failing in signing/signature verification --- autotests/CMakeLists.txt | 1 + autotests/testolmaccount.cpp | 10 ++-- autotests/testolmutility.cpp | 131 +++++++++++++++++++++++++++++++++++++++++++ autotests/testolmutility.h | 15 +++++ lib/crypto/qolmaccount.cpp | 36 ++++++------ lib/crypto/qolmaccount.h | 8 ++- lib/crypto/qolmutility.cpp | 23 ++++++-- lib/crypto/qolmutility.h | 2 +- 8 files changed, 196 insertions(+), 30 deletions(-) create mode 100644 autotests/testolmutility.cpp create mode 100644 autotests/testolmutility.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 6afdf8cc..0354172b 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -16,4 +16,5 @@ if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testolmaccount) quotient_add_test(NAME testgroupsession) quotient_add_test(NAME testolmsession) + quotient_add_test(NAME testolmutility) endif() diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 5cb88a99..8d979e0b 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -309,11 +309,10 @@ void TestOlmAccount::claimKeys() deviceKeys[bob->userId()] = QStringList(); auto job = alice->callApi(deviceKeys); connect(job, &BaseJob::result, this, [bob, alice, aliceOlm, job, this] { + qDebug() << job->jsonData(); auto bobDevices = job->deviceKeys()[bob->userId()]; QVERIFY(bobDevices.size() > 0); - auto devices = {bob->deviceId()}; - // Retrieve the identity key for the current device. auto bobEd25519 = bobDevices[bob->deviceId()].keys["ed25519:" + bob->deviceId()]; @@ -324,10 +323,9 @@ void TestOlmAccount::claimKeys() QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), bob->userId())); QHash> oneTimeKeys; - for (const auto &d : devices) { - oneTimeKeys[bob->userId()] = QHash(); - oneTimeKeys[bob->userId()][d] = SignedCurve25519Key; - } + oneTimeKeys[bob->userId()] = QHash(); + oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; + auto job = alice->callApi(oneTimeKeys); connect(job, &BaseJob::result, this, [aliceOlm, bob, bobEd25519, job] { const auto userId = bob->userId(); diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp new file mode 100644 index 00000000..cb92a0df --- /dev/null +++ b/autotests/testolmutility.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testolmutility.h" +#include "crypto/qolmaccount.h" +#include "crypto/qolmutility.h" + +using namespace Quotient; + +void TestOlmUtility::canonicalJSON() +{ + // Examples taken from + // https://matrix.org/docs/spec/appendices.html#canonical-json + auto data = QJsonDocument::fromJson(QByteArrayLiteral(R"({ + "auth": { + "success": true, + "mxid": "@john.doe:example.com", + "profile": { + "display_name": "John Doe", + "three_pids": [{ + "medium": "email", + "address": "john.doe@example.org" + }, { + "medium": "msisdn", + "address": "123456789" + }] + }}})")); + + QCOMPARE(data.toJson(QJsonDocument::Compact), + "{\"auth\":{\"mxid\":\"@john.doe:example.com\",\"profile\":{\"display_name\":\"John " + "Doe\",\"three_pids\":[{\"address\":\"john.doe@example.org\",\"medium\":\"email\"},{" + "\"address\":\"123456789\",\"medium\":\"msisdn\"}]},\"success\":true}}"); + + auto data0 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"b":"2","a":"1"})")); + QCOMPARE(data0.toJson(QJsonDocument::Compact), "{\"a\":\"1\",\"b\":\"2\"}"); + + auto data1 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "本": 2, "日": 1 })")); + QCOMPARE(data1.toJson(QJsonDocument::Compact), "{\"日\":1,\"本\":2}"); + + auto data2 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"a": "\u65E5"})")); + QCOMPARE(data2.toJson(QJsonDocument::Compact), "{\"a\":\"日\"}"); + + auto data3 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "a": null })")); + QCOMPARE(data3.toJson(QJsonDocument::Compact), "{\"a\":null}"); +} + +void TestOlmUtility::verifySignedOneTimeKey() +{ + auto aliceOlm = std::make_shared("alice:matrix.org", "aliceDevice"); + aliceOlm->createNewAccount(); + aliceOlm->generateOneTimeKeys(1); + auto keys = aliceOlm->oneTimeKeys(); + + auto firstKey = keys.curve25519().keyValueBegin()->second; + auto msgObj = QJsonObject({{"key", firstKey}}); + auto sig = aliceOlm->sign(msgObj); + + auto msg = QJsonDocument(msgObj).toJson(QJsonDocument::Compact); + + auto utilityBuf = new uint8_t[olm_utility_size()]; + auto utility = olm_utility(utilityBuf); + + qDebug() << "1" << aliceOlm->identityKeys().ed25519 << msg << QString::fromUtf8(sig); + + QByteArray signatureBuf1(sig.length(), '0'); + std::copy(sig.begin(), sig.end(), signatureBuf1.begin()); + + auto res = olm_ed25519_verify(utility, + aliceOlm->identityKeys().ed25519.data(), + aliceOlm->identityKeys().ed25519.size(), + msg.data(), + msg.size(), + (void *)sig.data(), + sig.size()); + qDebug() << "2" << aliceOlm->identityKeys().ed25519 << msg << QString::fromUtf8(signatureBuf1); + + QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); + QCOMPARE(res, 0); + + delete[](reinterpret_cast(utility)); + + QOlmUtility utility2; + auto res2 = std::get(utility2.ed25519Verify(aliceOlm->identityKeys().ed25519, msg, signatureBuf1)); + + //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); + QCOMPARE(res2, true); +} + +void TestOlmUtility::validUploadKeysRequest() +{ + const auto userId = QStringLiteral("@alice:matrix.org"); + const auto deviceId = QStringLiteral("FKALSOCCC"); + + auto alice = std::make_shared(userId, deviceId); + alice->createNewAccount(); + alice->generateOneTimeKeys(1); + + auto idSig = alice->signIdentityKeys(); + + QJsonObject body + { + {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, + {"user_id", userId}, + {"device_id", deviceId}, + {"keys", + QJsonObject{ + {QStringLiteral("curve25519:") + deviceId, QString::fromUtf8(alice->identityKeys().curve25519)}, + {QStringLiteral("ed25519:") + deviceId, QString::fromUtf8(alice->identityKeys().ed25519)} + } + }, + {"signatures", + QJsonObject{ + {userId, + QJsonObject{ + {"ed25519:" + deviceId, QString::fromUtf8(idSig)} + } + } + } + } + }; + + DeviceKeys deviceKeys = alice->getDeviceKeys(); + QCOMPARE(QJsonDocument(toJson(deviceKeys)).toJson(QJsonDocument::Compact), + QJsonDocument(body).toJson(QJsonDocument::Compact)); + + QVERIFY(verifyIdentitySignature(fromJson(body), deviceId, userId)); + QVERIFY(verifyIdentitySignature(deviceKeys, deviceId, userId)); +} + +QTEST_MAIN(TestOlmUtility) diff --git a/autotests/testolmutility.h b/autotests/testolmutility.h new file mode 100644 index 00000000..b30249c8 --- /dev/null +++ b/autotests/testolmutility.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +class TestOlmUtility : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void canonicalJSON(); + void verifySignedOneTimeKey(); + void validUploadKeysRequest(); +}; diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 750d7318..e27bbee1 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -110,10 +110,10 @@ IdentityKeys QOlmAccount::identityKeys() const QByteArray QOlmAccount::sign(const QByteArray &message) const { - const size_t signatureLength = olm_account_signature_length(m_account); - QByteArray signatureBuffer(signatureLength, '0'); + QByteArray signatureBuffer(olm_account_signature_length(m_account), '0'); + const auto error = olm_account_sign(m_account, message.data(), message.length(), - signatureBuffer.data(), signatureLength); + signatureBuffer.data(), signatureBuffer.length()); if (error == olm_error()) { throw lastError(m_account); @@ -216,9 +216,8 @@ OlmAccount *Quotient::QOlmAccount::data() return m_account; } -UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) +DeviceKeys QOlmAccount::getDeviceKeys() const { - DeviceKeys deviceKeys; deviceKeys.userId = m_userId; deviceKeys.deviceId = m_deviceId; @@ -231,6 +230,13 @@ UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKey const auto sign = signIdentityKeys(); deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; + return deviceKeys; +} + +UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) +{ + auto deviceKeys = getDeviceKeys(); + if (oneTimeKeys.curve25519().isEmpty()) { return new UploadKeysJob(deviceKeys); } @@ -272,36 +278,34 @@ bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, const auto signingKey = deviceKeys.keys[signKeyId]; const auto signature = deviceKeys.signatures[userId][signKeyId]; - if (signature.isEmpty()) { + qDebug() << "signature empty"; return false; } return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); } -bool Quotient::ed25519VerifySignature(QString signingKey, - QJsonObject obj, - QString signature) +bool Quotient::ed25519VerifySignature(const QString &signingKey, + const QJsonObject &obj, + const QString &signature) { if (signature.isEmpty()) { return false; } + QJsonObject obj1 = obj; - obj.remove("unsigned"); - obj.remove("signatures"); - - QJsonDocument doc; - doc.setObject(obj); - auto canonicalJson = doc.toJson(QJsonDocument::Compact); + obj1.remove("unsigned"); + obj1.remove("signatures"); - qDebug() << canonicalJson; + auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); QByteArray signingKeyBuf = signingKey.toUtf8(); QOlmUtility utility; auto signatureBuf = signature.toUtf8(); auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); if (std::holds_alternative(result)) { + qDebug() << "error:" << std::get(result); return false; } diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index 09ef623a..de78a8af 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -72,6 +72,8 @@ public: UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); + DeviceKeys getDeviceKeys() const; + //! Remove the one time key used to create the supplied session. [[nodiscard]] std::optional removeOneTimeKeys(const std::unique_ptr &session) const; @@ -104,9 +106,9 @@ bool verifyIdentitySignature(const DeviceKeys &deviceKeys, const QString &userId); //! checks if the signature is signed by the signing_key -bool ed25519VerifySignature(QString signingKey, - QJsonObject obj, - QString signature); +bool ed25519VerifySignature(const QString &signingKey, + const QJsonObject &obj, + const QString &signature); } // namespace Quotient diff --git a/lib/crypto/qolmutility.cpp b/lib/crypto/qolmutility.cpp index 3c6a14c7..ad78a226 100644 --- a/lib/crypto/qolmutility.cpp +++ b/lib/crypto/qolmutility.cpp @@ -5,6 +5,7 @@ #ifdef Quotient_E2EE_ENABLED #include "crypto/qolmutility.h" #include "olm/olm.h" +#include using namespace Quotient; @@ -19,10 +20,12 @@ QOlmUtility::QOlmUtility() { auto utility = new uint8_t[olm_utility_size()]; m_utility = olm_utility(utility); + qDebug() << "created"; } QOlmUtility::~QOlmUtility() { + qDebug() << "deleted"; olm_clear_utility(m_utility); delete[](reinterpret_cast(m_utility)); } @@ -43,15 +46,27 @@ QString QOlmUtility::sha256Utf8Msg(const QString &message) const } std::variant QOlmUtility::ed25519Verify(const QByteArray &key, - const QByteArray &message, QByteArray &signature) + const QByteArray &message, const QByteArray &signature) { - const auto error = olm_ed25519_verify(m_utility, key.data(), key.length(), - message.data(), message.length(), signature.data(), signature.length()); + QByteArray signatureBuf(signature.length(), '0'); + std::copy(signature.begin(), signature.end(), signatureBuf.begin()); + qDebug() << "3" << key << message << signature; + + const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), + message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); + + const auto error = ret; if (error == olm_error()) { + qDebug() << QString(olm_utility_last_error(m_utility)); return lastError(m_utility); } - return error == 0; + + if (ret != 0) { + qDebug() << "ed25519Verify" << ret; + return false; + } + return true; } diff --git a/lib/crypto/qolmutility.h b/lib/crypto/qolmutility.h index 16c330eb..3de09ab4 100644 --- a/lib/crypto/qolmutility.h +++ b/lib/crypto/qolmutility.h @@ -36,7 +36,7 @@ public: //! \param message QByteArray The message that was signed. //! \param signature QByteArray The signature of the message. std::variant ed25519Verify(const QByteArray &key, - const QByteArray &message, QByteArray &signature); + const QByteArray &message, const QByteArray &signature); private: -- cgit v1.2.3 From a5e84c51e9c89021edc8aaade8c751fb6d39cb89 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 31 Jan 2021 01:20:57 +0100 Subject: remove old files --- lib/crypto/qolmsession.cpp.back | 29 ------------------------ lib/crypto/qolmsession.h.back | 49 ----------------------------------------- 2 files changed, 78 deletions(-) delete mode 100644 lib/crypto/qolmsession.cpp.back delete mode 100644 lib/crypto/qolmsession.h.back diff --git a/lib/crypto/qolmsession.cpp.back b/lib/crypto/qolmsession.cpp.back deleted file mode 100644 index ee8b2a7f..00000000 --- a/lib/crypto/qolmsession.cpp.back +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "olm/qolmsession.h" - -using namespace Quotient; - -std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext) -{ - if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) { - return PreKeyMessage { ciphertext }; - } else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) { - return QOlmMessage { ciphertext }; - } - return std::nullopt; -} - -std::pair toPair(const OlmMessage &message) -{ - return std::visit([](auto &arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return std::make_pair(MessageType, QByteArray(arg.message)); - } else if constexpr (std::is_same_v) { - return std::make_pair(PreKeyType, QByteArray(arg.message)); - } - }, message); -} diff --git a/lib/crypto/qolmsession.h.back b/lib/crypto/qolmsession.h.back deleted file mode 100644 index cbba5cef..00000000 --- a/lib/crypto/qolmsession.h.back +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "olm/e2ee.h" -#include "olm/olm.h" -#include "olm/errors.h" -#include - -namespace Quotient { - -//! An encrypted Olm message. -struct QOlmMessage { - QByteArray message; -}; - -//! A encrypted Olm pre-key message. -//! -//! This message, unlike a normal Message, can be used to create new Olm sessions. -struct PreKeyMessage -{ - QByteArray message; -}; - -enum OlmMessageType -{ - PreKeyType, - MessageType, -}; - -using OlmMessage = std::variant; - -std::optional fromTypeAndCipthertext(size_t messageType, const QByteArray &ciphertext); - -std::pair toPair(const OlmMessage &message); - -//class QOlmSession -//{ -// /// Creates an inbound session for sending/receiving messages from a received 'prekey' message. -// static std::variant, OlmError> createInboundSession(const QOlmAccount &account, -// PreKeyMessage &message); -// -////private: -// //static std::variant, OlmError> createSessionWith(std::function> func); -//} - -} -- cgit v1.2.3 From d9dc94a8fb59c8590c4aa7cdf773c2825e69d823 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sun, 31 Jan 2021 21:11:23 +0100 Subject: Fix signing keys --- autotests/testolmaccount.cpp | 12 ++++++++++-- autotests/testolmutility.cpp | 2 -- lib/crypto/qolmaccount.cpp | 19 +++++++++++++------ lib/crypto/qolmutility.cpp | 6 ------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 8d979e0b..4eed1980 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -309,7 +309,6 @@ void TestOlmAccount::claimKeys() deviceKeys[bob->userId()] = QStringList(); auto job = alice->callApi(deviceKeys); connect(job, &BaseJob::result, this, [bob, alice, aliceOlm, job, this] { - qDebug() << job->jsonData(); auto bobDevices = job->deviceKeys()[bob->userId()]; QVERIFY(bobDevices.size() > 0); @@ -337,7 +336,16 @@ void TestOlmAccount::claimKeys() // The key is the one bob sent. auto oneTimeKey = job->oneTimeKeys()[userId][deviceId]; - QVERIFY(oneTimeKey.canConvert()); + QVERIFY(oneTimeKey.canConvert()); + + QVariantMap varMap = oneTimeKey.toMap(); + bool found = false; + for (const auto key : varMap.keys()) { + if (key.startsWith(QStringLiteral("signed_curve25519"))) { + found = true; + } + } + QVERIFY(found); //auto algo = oneTimeKey.begin().key(); //auto contents = oneTimeKey.begin().value(); diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index cb92a0df..1d9978d3 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -61,7 +61,6 @@ void TestOlmUtility::verifySignedOneTimeKey() auto utilityBuf = new uint8_t[olm_utility_size()]; auto utility = olm_utility(utilityBuf); - qDebug() << "1" << aliceOlm->identityKeys().ed25519 << msg << QString::fromUtf8(sig); QByteArray signatureBuf1(sig.length(), '0'); std::copy(sig.begin(), sig.end(), signatureBuf1.begin()); @@ -73,7 +72,6 @@ void TestOlmUtility::verifySignedOneTimeKey() msg.size(), (void *)sig.data(), sig.size()); - qDebug() << "2" << aliceOlm->identityKeys().ed25519 << msg << QString::fromUtf8(signatureBuf1); QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); QCOMPARE(res, 0); diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index e27bbee1..0f354d3e 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -129,10 +129,19 @@ QByteArray QOlmAccount::sign(const QJsonObject &message) const QByteArray QOlmAccount::signIdentityKeys() const { const auto keys = identityKeys(); - const QJsonObject j{ {Curve25519Key, QString(keys.curve25519)}, {Ed25519Key, QString(keys.ed25519)} }; - QJsonDocument doc; - doc.setObject(j); - return sign(doc.toJson()); + QJsonObject body + { + {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, + {"user_id", m_userId}, + {"device_id", m_deviceId}, + {"keys", + QJsonObject{ + {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)}, + {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)} + } + } + }; + return sign(QJsonDocument(body).toJson(QJsonDocument::Compact)); } @@ -279,7 +288,6 @@ bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, const auto signature = deviceKeys.signatures[userId][signKeyId]; if (signature.isEmpty()) { - qDebug() << "signature empty"; return false; } @@ -305,7 +313,6 @@ bool Quotient::ed25519VerifySignature(const QString &signingKey, auto signatureBuf = signature.toUtf8(); auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); if (std::holds_alternative(result)) { - qDebug() << "error:" << std::get(result); return false; } diff --git a/lib/crypto/qolmutility.cpp b/lib/crypto/qolmutility.cpp index ad78a226..87615770 100644 --- a/lib/crypto/qolmutility.cpp +++ b/lib/crypto/qolmutility.cpp @@ -20,12 +20,10 @@ QOlmUtility::QOlmUtility() { auto utility = new uint8_t[olm_utility_size()]; m_utility = olm_utility(utility); - qDebug() << "created"; } QOlmUtility::~QOlmUtility() { - qDebug() << "deleted"; olm_clear_utility(m_utility); delete[](reinterpret_cast(m_utility)); } @@ -51,19 +49,15 @@ std::variant QOlmUtility::ed25519Verify(const QByteArray &key, QByteArray signatureBuf(signature.length(), '0'); std::copy(signature.begin(), signature.end(), signatureBuf.begin()); - qDebug() << "3" << key << message << signature; - const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); const auto error = ret; if (error == olm_error()) { - qDebug() << QString(olm_utility_last_error(m_utility)); return lastError(m_utility); } if (ret != 0) { - qDebug() << "ed25519Verify" << ret; return false; } return true; -- cgit v1.2.3 From 38547289d56cf66b4f1384ae789cf5b6cd71763e Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 5 Feb 2021 00:03:27 +0100 Subject: Fix cmake code --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fb07fa22..ba6c8cd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,7 +316,8 @@ if (${PROJECT_NAME}_ENABLE_E2EE) target_link_libraries(${PROJECT_NAME} Olm::Olm OpenSSL::Crypto OpenSSL::SSL) - set(FIND_DEPS "find_dependency(Olm OpenSSL)") # For QuotientConfig.cmake.in + set(FIND_DEPS "find_dependency(Olm) + find_dependency(OpenSSL)") # For QuotientConfig.cmake.in endif() target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) -- 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/converters.cpp | 6 +++--- lib/crypto/qolmutils.cpp | 2 +- lib/encryptionmanager.cpp | 4 ++-- lib/room.cpp | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/converters.cpp b/lib/converters.cpp index e6dcd854..a3ac44c5 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -17,10 +17,10 @@ QJsonValue Quotient::JsonConverter::dump(const QVariant& v) QVariant Quotient::JsonConverter::load(const QJsonValue& jv) { if (jv.isObject()) { - QJsonObject obj = jv.toObject(); - if (obj.contains("key") && obj.contains("signatures")) { + const QJsonObject obj = jv.toObject(); + if (obj.contains(QLatin1String("key")) && obj.contains(QLatin1String("signatures"))) { SignedOneTimeKey signedOneTimeKeys; - signedOneTimeKeys.key = obj["key"].toString(); + signedOneTimeKeys.key = obj[QLatin1String("key")].toString(); } } return jv.toVariant(); diff --git a/lib/crypto/qolmutils.cpp b/lib/crypto/qolmutils.cpp index a486ea0f..4479932e 100644 --- a/lib/crypto/qolmutils.cpp +++ b/lib/crypto/qolmutils.cpp @@ -12,7 +12,7 @@ using namespace Quotient; QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) { if (std::holds_alternative(mode)) { - return ""; + return {}; } return std::get(mode).key; } diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index c8dc6bdd..719add1d 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -333,10 +333,10 @@ QString EncryptionManager::sessionDecryptMessage( int type = personalCipherObject.value(TypeKeyL).toInt(-1); QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); if (type == 0) { - QOlmMessage preKeyMessage = QOlmMessage(body, QOlmMessage::PreKey); + QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); decrypted = d->sessionDecrypt(preKeyMessage, senderKey); } else if (type == 1) { - QOlmMessage message = QOlmMessage(body, QOlmMessage::PreKey); + QOlmMessage message(body, QOlmMessage::PreKey); decrypted = d->sessionDecrypt(message, senderKey); } return decrypted; 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 --- autotests/testolmaccount.cpp | 4 ++-- autotests/testolmaccount.h | 2 +- lib/encryptionmanager.cpp | 2 +- lib/room.cpp | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 4eed1980..8129ae5b 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -9,7 +9,7 @@ using namespace Quotient; -void TestOlmAccount::pickleUnpickedTest() +void TestOlmAccount::pickleUnpickledTest() { QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); olmAccount.createNewAccount(); @@ -73,7 +73,7 @@ void TestOlmAccount::deviceKeys() { // copied from mtxclient DeviceKeys device1; - device1.userId = "@alice:example.com"; + device1.userId = "@alice:example.com"; device1.deviceId = "JLAFKJWSCS"; device1.keys = {{"curve25519:JLAFKJWSCS", "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"}, {"ed25519:JLAFKJWSCS", "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"}}; diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index b6b9cae4..97fbca18 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -15,7 +15,7 @@ class TestOlmAccount : public QObject private Q_SLOTS: - void pickleUnpickedTest(); + void pickleUnpickledTest(); void identityKeysValid(); void signatureValid(); void oneTimeKeysValid(); diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 719add1d..3c3103a7 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -69,7 +69,7 @@ public: UploadKeysJob* uploadOneTimeKeysJob = nullptr; QueryKeysJob* queryKeysJob = nullptr; - QScopedPointer olmAccount; + std::unique_ptr olmAccount; float signedKeysProportion; float oneTimeKeyThreshold; 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 5c50f0ccadedbcb07d51dbac9b1d59c03a26af2f Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 15 Feb 2021 18:13:27 +0100 Subject: fix typo --- autotests/testolmaccount.cpp | 1 - lib/encryptionmanager.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 8129ae5b..91342241 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -187,7 +187,6 @@ void TestOlmAccount::uploadIdentityKey() auto olmAccount = conn->olmAccount(); auto idKeys = olmAccount->identityKeys(); - QVERIFY(idKeys.curve25519.size() > 10); QVERIFY(idKeys.curve25519.size() > 10); OneTimeKeys unused; diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 3c3103a7..53890fdb 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -350,7 +350,7 @@ QByteArray EncryptionManager::olmAccountPickle() QOlmAccount *EncryptionManager::account() const { - return d->olmAccount.data(); + return d->olmAccount.get(); } void EncryptionManager::Private::updateKeysToUpload() -- cgit v1.2.3 From bc4ef60c29709a6f782f6e75e1f040f250fb8bd7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 22 Mar 2021 16:32:21 +0100 Subject: Ifdef E2EE out at cmake level --- CMakeLists.txt | 22 +++++++++++++--------- lib/crypto/qolmaccount.cpp | 3 --- lib/crypto/qolmaccount.h | 4 +--- lib/crypto/qolmerrors.cpp | 4 ++-- lib/crypto/qolmerrors.h | 3 --- lib/crypto/qolminboundsession.cpp | 2 -- lib/crypto/qolminboundsession.h | 3 --- lib/crypto/qolmmessage.cpp | 7 ------- lib/crypto/qolmmessage.h | 5 ----- lib/crypto/qolmoutboundsession.cpp | 3 --- lib/crypto/qolmoutboundsession.h | 3 +-- lib/crypto/qolmsession.cpp | 6 ------ lib/crypto/qolmsession.h | 4 ---- lib/crypto/qolmutility.cpp | 4 ---- lib/crypto/qolmutility.h | 3 --- lib/crypto/qolmutils.cpp | 2 -- lib/crypto/qolmutils.h | 2 -- 17 files changed, 17 insertions(+), 63 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ba6c8cd1..5601a281 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,16 +158,20 @@ list(APPEND lib_SRCS lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp - lib/crypto/qolmaccount.cpp - lib/crypto/qolmsession.cpp - lib/crypto/qolminboundsession.cpp - lib/crypto/qolmoutboundsession.cpp - lib/crypto/qolmutils.cpp - lib/crypto/qolmutility.cpp - lib/crypto/qolmerrors.cpp - lib/crypto/qolmsession.cpp - lib/crypto/qolmmessage.cpp ) +if (${PROJECT_NAME}_ENABLE_E2EE) + list(APPEND lib_SRCS + lib/crypto/qolmaccount.cpp + lib/crypto/qolmsession.cpp + lib/crypto/qolminboundsession.cpp + lib/crypto/qolmoutboundsession.cpp + lib/crypto/qolmutils.cpp + lib/crypto/qolmutility.cpp + lib/crypto/qolmerrors.cpp + lib/crypto/qolmsession.cpp + lib/crypto/qolmmessage.cpp + ) +endif() # Configure API files generation diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 0f354d3e..4f007e2f 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "qolmaccount.h" #include "connection.h" #include "csapi/keys.h" @@ -318,5 +317,3 @@ bool Quotient::ed25519VerifySignature(const QString &signingKey, return std::get(result); } - -#endif diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index de78a8af..1e198687 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan // // SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once -#ifdef Quotient_E2EE_ENABLED #include "csapi/keys.h" #include "crypto/e2ee.h" @@ -111,5 +111,3 @@ bool ed25519VerifySignature(const QString &signingKey, const QString &signature); } // namespace Quotient - -#endif diff --git a/lib/crypto/qolmerrors.cpp b/lib/crypto/qolmerrors.cpp index f407383e..46b2618c 100644 --- a/lib/crypto/qolmerrors.cpp +++ b/lib/crypto/qolmerrors.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan +// // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED + #include "qolmerrors.h" Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { @@ -18,4 +19,3 @@ Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { return QOlmError::Unknown; } } -#endif diff --git a/lib/crypto/qolmerrors.h b/lib/crypto/qolmerrors.h index 400573c6..f8390d2a 100644 --- a/lib/crypto/qolmerrors.h +++ b/lib/crypto/qolmerrors.h @@ -4,7 +4,6 @@ #pragma once -#ifdef Quotient_E2EE_ENABLED #include namespace Quotient { @@ -27,5 +26,3 @@ enum QOlmError QOlmError fromString(const std::string &error_raw); } //namespace Quotient - -#endif diff --git a/lib/crypto/qolminboundsession.cpp b/lib/crypto/qolminboundsession.cpp index 8f5056d8..e1ced72b 100644 --- a/lib/crypto/qolminboundsession.cpp +++ b/lib/crypto/qolminboundsession.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "crypto/qolminboundsession.h" #include #include @@ -154,4 +153,3 @@ bool QOlmInboundGroupSession::isVerified() const { return olm_inbound_group_session_is_verified(m_groupSession) != 0; } -#endif diff --git a/lib/crypto/qolminboundsession.h b/lib/crypto/qolminboundsession.h index 6af71cbd..36ab4942 100644 --- a/lib/crypto/qolminboundsession.h +++ b/lib/crypto/qolminboundsession.h @@ -4,8 +4,6 @@ #pragma once -#ifdef Quotient_E2EE_ENABLED - #include #include #include @@ -48,4 +46,3 @@ private: using QOlmInboundGroupSessionPtr = std::unique_ptr; using OlmInboundGroupSessionPtr = std::unique_ptr; } // namespace Quotient -#endif diff --git a/lib/crypto/qolmmessage.cpp b/lib/crypto/qolmmessage.cpp index ae98d52f..15008b75 100644 --- a/lib/crypto/qolmmessage.cpp +++ b/lib/crypto/qolmmessage.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "qolmmessage.h" using namespace Quotient; @@ -34,9 +33,3 @@ QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) { return QOlmMessage(ciphertext, QOlmMessage::General); } - - -#endif // Quotient_E2EE_ENABLED - - - diff --git a/lib/crypto/qolmmessage.h b/lib/crypto/qolmmessage.h index d203364d..52aba78c 100644 --- a/lib/crypto/qolmmessage.h +++ b/lib/crypto/qolmmessage.h @@ -4,8 +4,6 @@ #pragma once -#ifdef Quotient_E2EE_ENABLED - #include #include @@ -40,7 +38,4 @@ private: Type m_messageType = General; }; - } //namespace Quotient - -#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmoutboundsession.cpp b/lib/crypto/qolmoutboundsession.cpp index 14b7368e..bf8dce61 100644 --- a/lib/crypto/qolmoutboundsession.cpp +++ b/lib/crypto/qolmoutboundsession.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "qolmoutboundsession.h" #include "crypto/qolmutils.h" @@ -127,5 +126,3 @@ std::variant QOlmOutboundGroupSession::sessionKey() const } return keyBuffer; } - -#endif diff --git a/lib/crypto/qolmoutboundsession.h b/lib/crypto/qolmoutboundsession.h index 6b4fd30b..f1df0395 100644 --- a/lib/crypto/qolmoutboundsession.h +++ b/lib/crypto/qolmoutboundsession.h @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan // // SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once -#ifdef Quotient_E2EE_ENABLED #include "olm/olm.h" #include "crypto/qolmerrors.h" @@ -51,4 +51,3 @@ private: using QOlmOutboundGroupSessionPtr = std::unique_ptr; using OlmOutboundGroupSessionPtr = std::unique_ptr; } -#endif diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp index b901a440..a1f6ab71 100644 --- a/lib/crypto/qolmsession.cpp +++ b/lib/crypto/qolmsession.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "qolmsession.h" #include "crypto/qolmutils.h" #include "logging.h" @@ -255,8 +254,3 @@ QOlmSession::QOlmSession(OlmSession *session) : m_session(session) { } - -#endif // Quotient_E2EE_ENABLED - - - diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h index 0fc59e9e..959c77d0 100644 --- a/lib/crypto/qolmsession.h +++ b/lib/crypto/qolmsession.h @@ -4,8 +4,6 @@ #pragma once -#ifdef Quotient_E2EE_ENABLED - #include #include // FIXME: OlmSession #include "crypto/e2ee.h" @@ -80,5 +78,3 @@ private: //using QOlmSessionPtr = std::unique_ptr; } //namespace Quotient - -#endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmutility.cpp b/lib/crypto/qolmutility.cpp index 87615770..bb50b4d0 100644 --- a/lib/crypto/qolmutility.cpp +++ b/lib/crypto/qolmutility.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "crypto/qolmutility.h" #include "olm/olm.h" #include @@ -62,6 +61,3 @@ std::variant QOlmUtility::ed25519Verify(const QByteArray &key, } return true; } - - -#endif diff --git a/lib/crypto/qolmutility.h b/lib/crypto/qolmutility.h index 3de09ab4..fc6569f7 100644 --- a/lib/crypto/qolmutility.h +++ b/lib/crypto/qolmutility.h @@ -4,7 +4,6 @@ #pragma once -#ifdef Quotient_E2EE_ENABLED #include #include #include "crypto/qolmerrors.h" @@ -44,5 +43,3 @@ private: }; } - -#endif diff --git a/lib/crypto/qolmutils.cpp b/lib/crypto/qolmutils.cpp index 4479932e..cd5ac83c 100644 --- a/lib/crypto/qolmutils.cpp +++ b/lib/crypto/qolmutils.cpp @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifdef Quotient_E2EE_ENABLED #include "crypto/qolmutils.h" #include #include @@ -23,4 +22,3 @@ QByteArray Quotient::getRandom(size_t bufferSize) RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); return buffer; } -#endif diff --git a/lib/crypto/qolmutils.h b/lib/crypto/qolmutils.h index 11e9f3cc..8b1c01ce 100644 --- a/lib/crypto/qolmutils.h +++ b/lib/crypto/qolmutils.h @@ -3,7 +3,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#ifdef Quotient_E2EE_ENABLED #include @@ -14,4 +13,3 @@ namespace Quotient { QByteArray toKey(const PicklingMode &mode); QByteArray getRandom(size_t bufferSize); } -#endif -- cgit v1.2.3 From ea617d31cf3f72f76fd49c0a20f445a78678fe5f Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 19 Apr 2021 16:07:57 +0200 Subject: Apply suggestions from code review Co-authored-by: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> --- autotests/testgroupsession.cpp | 1 - autotests/testgroupsession.h | 2 +- autotests/testolmaccount.h | 3 +-- autotests/testolmsession.cpp | 2 +- autotests/testolmsession.h | 2 +- autotests/testolmutility.h | 2 +- lib/crypto/qolmaccount.cpp | 16 +++++++--------- lib/crypto/qolmaccount.h | 7 ++++--- lib/crypto/qolmerrors.cpp | 1 + lib/crypto/qolminboundsession.cpp | 8 +++----- lib/crypto/qolmoutboundsession.cpp | 8 ++++---- lib/crypto/qolmoutboundsession.h | 5 +++-- lib/crypto/qolmutility.h | 4 ++-- 13 files changed, 29 insertions(+), 32 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 858f29d8..ea1bb4a9 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -41,7 +41,6 @@ void TestOlmSession::groupSessionCryptoValid() const auto plainText = QStringLiteral("Hello world!"); const auto ciphertext = std::get(ogs->encrypt(plainText)); - qDebug() << ciphertext; // ciphertext valid base64? QVERIFY(QByteArray::fromBase64Encoding(ciphertext).decodingStatus == QByteArray::Base64DecodingStatus::Ok); diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h index 27f34bec..7743295f 100644 --- a/autotests/testgroupsession.h +++ b/autotests/testgroupsession.h @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include class TestOlmSession : public QObject { diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 97fbca18..8901f095 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include #include namespace Quotient { @@ -13,7 +13,6 @@ class TestOlmAccount : public QObject { Q_OBJECT - private Q_SLOTS: void pickleUnpickledTest(); void identityKeysValid(); diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 72c54174..0803cc6d 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -26,7 +26,7 @@ std::pair, std::unique_ptr> createSess const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey if (preKey.type() != QOlmMessage::PreKey) { - throw "Wrong first message type received, can't create session"; + QFAIL("Wrong first message type received, can't create session"); } auto inbound = std::get>(accountB.createInboundSession(preKey)); return std::make_pair, std::unique_ptr>(std::move(inbound), std::move(outbound)); diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h index 9a5798fa..bd670c9b 100644 --- a/autotests/testolmsession.h +++ b/autotests/testolmsession.h @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include class TestOlmSession : public QObject { diff --git a/autotests/testolmutility.h b/autotests/testolmutility.h index b30249c8..f2a3ca45 100644 --- a/autotests/testolmutility.h +++ b/autotests/testolmutility.h @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include class TestOlmUtility : public QObject { diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 4f007e2f..8b964c9f 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -151,9 +151,9 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const { - const size_t randomLen = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); - QByteArray randomBuffer = getRandom(randomLen); - const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLen); + const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + QByteArray randomBuffer = getRandom(randomLength); + const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); if (error == olm_error()) { throw lastError(m_account); @@ -219,12 +219,12 @@ std::optional QOlmAccount::removeOneTimeKeys(const std::unique_ptr oneTimeKeysSigned; for (const auto &[keyId, key] : asKeyValueRange(temp)) { - QVariant keyVar; - keyVar.setValue(key); - oneTimeKeysSigned[keyId] = keyVar; + oneTimeKeysSigned[keyId] = QVariant::fromValue(key); } return new UploadKeysJob(deviceKeys, oneTimeKeysSigned); diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index 1e198687..c93a8354 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "csapi/keys.h" @@ -37,7 +38,7 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. //! This needs to be called before any other action or use createNewAccount() instead. - void unpickle(QByteArray &picked, const PicklingMode &mode); + void unpickle(QByteArray &pickled, const PicklingMode &mode); //! Serialises an OlmAccount to encrypted Base64. std::variant pickle(const PicklingMode &mode); @@ -62,7 +63,7 @@ public: //! Gets the OlmAccount's one time keys formatted as JSON. OneTimeKeys oneTimeKeys() const; - //! Sign all time key. + //! Sign all one time keys. QMap signOneTimeKeys(const OneTimeKeys &keys) const; //! Sign one time key. @@ -84,7 +85,7 @@ public: //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! - //! \param theirIdentityKey - The identity key of an Olm account that + //! \param theirIdentityKey - The identity key of the Olm account that //! encrypted this Olm message. std::variant, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); diff --git a/lib/crypto/qolmerrors.cpp b/lib/crypto/qolmerrors.cpp index 46b2618c..2c3926de 100644 --- a/lib/crypto/qolmerrors.cpp +++ b/lib/crypto/qolmerrors.cpp @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later + #include "qolmerrors.h" Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { diff --git a/lib/crypto/qolminboundsession.cpp b/lib/crypto/qolminboundsession.cpp index e1ced72b..beaf3299 100644 --- a/lib/crypto/qolminboundsession.cpp +++ b/lib/crypto/qolminboundsession.cpp @@ -10,7 +10,6 @@ using namespace Quotient; QOlmError lastError(OlmInboundGroupSession *session) { const std::string error_raw = olm_inbound_group_session_last_error(session); - std::cout << error_raw; return fromString(error_raw); } @@ -39,7 +38,6 @@ std::unique_ptr QOlmInboundGroupSession::create(const Q return std::make_unique(olmInboundGroupSession); } - std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); @@ -123,9 +121,9 @@ std::variant, QOlmError> QOlmInboundGroupSession::d std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) { - const auto keyLen = olm_export_inbound_group_session_length(m_groupSession); - QByteArray keyBuf(keyLen, '0'); - const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLen, messageIndex); + const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLength, '0'); + const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLength, messageIndex); if (error == olm_error()) { return lastError(m_groupSession); diff --git a/lib/crypto/qolmoutboundsession.cpp b/lib/crypto/qolmoutboundsession.cpp index bf8dce61..bc572ba5 100644 --- a/lib/crypto/qolmoutboundsession.cpp +++ b/lib/crypto/qolmoutboundsession.cpp @@ -27,8 +27,8 @@ QOlmOutboundGroupSession::~QOlmOutboundGroupSession() std::unique_ptr QOlmOutboundGroupSession::create() { auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - const auto randomLen = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); - QByteArray randomBuf = getRandom(randomLen); + const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); + QByteArray randomBuf = getRandom(randomLength); const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, reinterpret_cast(randomBuf.data()), randomBuf.length()); @@ -86,8 +86,8 @@ std::variant, QOlmError> QOlmOutboundG std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) { QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLen = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLen, '0'); + const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLength, '0'); const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); diff --git a/lib/crypto/qolmoutboundsession.h b/lib/crypto/qolmoutboundsession.h index f1df0395..201a178a 100644 --- a/lib/crypto/qolmoutboundsession.h +++ b/lib/crypto/qolmoutboundsession.h @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "olm/olm.h" @@ -21,11 +22,11 @@ public: //! Creates a new instance of `QOlmOutboundGroupSession`. //! Throw OlmError on errors static std::unique_ptr create(); - //! Serialises an `QOlmOutboundGroupSession` to encrypted Base64. + //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + static std::variant, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. std::variant encrypt(const QString &plaintext); diff --git a/lib/crypto/qolmutility.h b/lib/crypto/qolmutility.h index fc6569f7..5fd28dcc 100644 --- a/lib/crypto/qolmutility.h +++ b/lib/crypto/qolmutility.h @@ -27,11 +27,11 @@ public: QString sha256Bytes(const QByteArray &inputBuf) const; //! Convenience function that converts the UTF-8 message - //! to bytes and then calls `sha256_bytes()`, returning its output. + //! to bytes and then calls `sha256Bytes()`, returning its output. QString sha256Utf8Msg(const QString &message) const; //! Verify a ed25519 signature. - //! \param any QByteArray The public part of the ed25519 key that signed the message. + //! \param key QByteArray The public part of the ed25519 key that signed the message. //! \param message QByteArray The message that was signed. //! \param signature QByteArray The signature of the message. std::variant ed25519Verify(const QByteArray &key, -- cgit v1.2.3 From c2836e007e2d46c0c20270b99ede5b78d2c7170b Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 22 Feb 2021 13:51:51 +0100 Subject: ssl --- autotests/testolmaccount.cpp | 1 + lib/connection.cpp | 5 +++++ lib/connection.h | 5 +++++ lib/connectiondata.cpp | 6 ++++++ lib/connectiondata.h | 1 + lib/networkaccessmanager.cpp | 17 ++++++++++++----- lib/networkaccessmanager.h | 1 + 7 files changed, 31 insertions(+), 5 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 91342241..eb44791a 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -165,6 +165,7 @@ void TestOlmAccount::encryptedFile() #define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ auto VAR = std::make_shared(); \ + VAR->ignoreSslErrors(true); \ (VAR) ->resolveServer("@alice:localhost:" + QString::number(443)); \ connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [this, VAR ] () { \ (VAR) ->loginWithPassword( (USERNAME) , SECRET , DEVICE_NAME , ""); \ diff --git a/lib/connection.cpp b/lib/connection.cpp index 704bc1b4..62427ae1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -572,6 +572,11 @@ void Connection::sync(int timeout) }); } +void Connection::ignoreSslErrors(bool ignore) +{ + connectionData()->ignoreSslErrors(ignore); +} + void Connection::syncLoop(int timeout) { if (d->syncLoopConnection && d->syncTimeout == timeout) { diff --git a/lib/connection.h b/lib/connection.h index 6729b23d..93e22da2 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -476,6 +476,11 @@ public: setUserFactory(defaultUserFactory()); } + /// Ignore ssl errors (usefull for automated testing with local synapse + /// instance). + /// \internal + void ignoreSslErrors(bool ignore); + public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index 87ad4577..672feb06 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -128,6 +128,12 @@ bool ConnectionData::needsToken(const QString& requestName) const != d->needToken.cend(); } +void ConnectionData::ignoreSslErrors(bool ignore) const +{ + auto quotientNam = static_cast(nam()); + quotientNam.ignoreSslErrors(ignore); +} + void ConnectionData::setDeviceId(const QString& deviceId) { d->deviceId = deviceId; diff --git a/lib/connectiondata.h b/lib/connectiondata.h index e16a2dac..203dc9e8 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -29,6 +29,7 @@ public: bool needsToken(const QString& requestName) const; QNetworkAccessManager* nam() const; + void ignoreSslErrors(bool ignore = true) const; void setBaseUrl(QUrl baseUrl); void setToken(QByteArray accessToken); void setDeviceId(const QString& deviceId); diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 293538ee..d0380cec 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -40,17 +40,24 @@ public: NetworkAccessManager::NetworkAccessManager(QObject* parent) : QNetworkAccessManager(parent), d(std::make_unique(this)) -{ - connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList &errors) { - reply->ignoreSslErrors(); - }); -} +{} QList NetworkAccessManager::ignoredSslErrors() const { return d->ignoredSslErrors; } +void NetworkAccessManager::ignoreSslErrors(bool ignore) const +{ + if (ignore) { + connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList &errors) { + reply->ignoreSslErrors(); + }); + } else { + disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr); + } +} + void NetworkAccessManager::addIgnoredSslError(const QSslError& error) { d->ignoredSslErrors << error; diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 87bc12a1..7643302f 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -19,6 +19,7 @@ public: QList ignoredSslErrors() const; void addIgnoredSslError(const QSslError& error); void clearIgnoredSslErrors(); + void ignoreSslErrors(bool ignore = true) const; /** Get a pointer to the singleton */ static NetworkAccessManager* instance(); -- cgit v1.2.3 From 0a75a095665101d4ffcbec10b43633eee5a0d6d3 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 6 May 2021 01:08:53 +0200 Subject: Fix everything --- autotests/testolmaccount.cpp | 3 ++- autotests/testolmaccount.h | 2 +- autotests/testolmsession.cpp | 3 ++- autotests/testolmsession.h | 2 +- autotests/testolmutility.cpp | 2 +- lib/connection.cpp | 5 ----- lib/connection.h | 5 ----- lib/connectiondata.cpp | 6 ------ lib/connectiondata.h | 1 - lib/crypto/qolmaccount.cpp | 6 +++--- lib/crypto/qolmaccount.h | 2 +- lib/crypto/qolmoutboundsession.h | 2 +- 12 files changed, 12 insertions(+), 27 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index eb44791a..1c296db9 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -6,6 +6,7 @@ #include "crypto/qolmaccount.h" #include "connection.h" #include "events/encryptedfile.h" +#include "networkaccessmanager.h" using namespace Quotient; @@ -164,8 +165,8 @@ void TestOlmAccount::encryptedFile() } #define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ + NetworkAccessManager::instance()->ignoreSslErrors(true); \ auto VAR = std::make_shared(); \ - VAR->ignoreSslErrors(true); \ (VAR) ->resolveServer("@alice:localhost:" + QString::number(443)); \ connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [this, VAR ] () { \ (VAR) ->loginWithPassword( (USERNAME) , SECRET , DEVICE_NAME , ""); \ diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 8901f095..bab9eed2 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include #include namespace Quotient { diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 0803cc6d..dba78277 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -26,7 +26,8 @@ std::pair, std::unique_ptr> createSess const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey if (preKey.type() != QOlmMessage::PreKey) { - QFAIL("Wrong first message type received, can't create session"); + // We can't call QFail here because it's an helper function returning a value + throw "Wrong first message type received, can't create session"; } auto inbound = std::get>(accountB.createInboundSession(preKey)); return std::make_pair, std::unique_ptr>(std::move(inbound), std::move(outbound)); diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h index bd670c9b..9a5798fa 100644 --- a/autotests/testolmsession.h +++ b/autotests/testolmsession.h @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include +#include class TestOlmSession : public QObject { diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 1d9978d3..2eec7e00 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -118,7 +118,7 @@ void TestOlmUtility::validUploadKeysRequest() } }; - DeviceKeys deviceKeys = alice->getDeviceKeys(); + DeviceKeys deviceKeys = alice->deviceKeys(); QCOMPARE(QJsonDocument(toJson(deviceKeys)).toJson(QJsonDocument::Compact), QJsonDocument(body).toJson(QJsonDocument::Compact)); diff --git a/lib/connection.cpp b/lib/connection.cpp index 62427ae1..704bc1b4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -572,11 +572,6 @@ void Connection::sync(int timeout) }); } -void Connection::ignoreSslErrors(bool ignore) -{ - connectionData()->ignoreSslErrors(ignore); -} - void Connection::syncLoop(int timeout) { if (d->syncLoopConnection && d->syncTimeout == timeout) { diff --git a/lib/connection.h b/lib/connection.h index 93e22da2..6729b23d 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -476,11 +476,6 @@ public: setUserFactory(defaultUserFactory()); } - /// Ignore ssl errors (usefull for automated testing with local synapse - /// instance). - /// \internal - void ignoreSslErrors(bool ignore); - public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index 672feb06..87ad4577 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -128,12 +128,6 @@ bool ConnectionData::needsToken(const QString& requestName) const != d->needToken.cend(); } -void ConnectionData::ignoreSslErrors(bool ignore) const -{ - auto quotientNam = static_cast(nam()); - quotientNam.ignoreSslErrors(ignore); -} - void ConnectionData::setDeviceId(const QString& deviceId) { d->deviceId = deviceId; diff --git a/lib/connectiondata.h b/lib/connectiondata.h index 203dc9e8..e16a2dac 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -29,7 +29,6 @@ public: bool needsToken(const QString& requestName) const; QNetworkAccessManager* nam() const; - void ignoreSslErrors(bool ignore = true) const; void setBaseUrl(QUrl baseUrl); void setToken(QByteArray accessToken); void setDeviceId(const QString& deviceId); diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 8b964c9f..9368de4f 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -243,10 +243,10 @@ DeviceKeys QOlmAccount::deviceKeys() const UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) { - auto deviceKeys = deviceKeys(); + auto keys = deviceKeys(); if (oneTimeKeys.curve25519().isEmpty()) { - return new UploadKeysJob(deviceKeys); + return new UploadKeysJob(keys); } // Sign & append the one time keys. @@ -256,7 +256,7 @@ UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKey oneTimeKeysSigned[keyId] = QVariant::fromValue(key); } - return new UploadKeysJob(deviceKeys, oneTimeKeysSigned); + return new UploadKeysJob(keys, oneTimeKeysSigned); } std::variant, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index c93a8354..f3ca82f0 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -73,7 +73,7 @@ public: UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); - DeviceKeys getDeviceKeys() const; + DeviceKeys deviceKeys() const; //! Remove the one time key used to create the supplied session. [[nodiscard]] std::optional removeOneTimeKeys(const std::unique_ptr &session) const; diff --git a/lib/crypto/qolmoutboundsession.h b/lib/crypto/qolmoutboundsession.h index 201a178a..4e06561e 100644 --- a/lib/crypto/qolmoutboundsession.h +++ b/lib/crypto/qolmoutboundsession.h @@ -26,7 +26,7 @@ public: std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); + static std::variant, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); //! Encrypts a plaintext message using the session. std::variant encrypt(const QString &plaintext); -- cgit v1.2.3 From 65877dc9fb6e024d456343d42ef55e0c5c8b67b3 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 24 May 2021 16:52:45 +0200 Subject: Upload one-time keys when their count is low --- lib/connection.cpp | 20 +++++++++----------- lib/crypto/qolmaccount.cpp | 5 +++++ lib/crypto/qolmaccount.h | 2 ++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 704bc1b4..9883b8f3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -620,17 +620,15 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->consumePresenceData(data.takePresenceData()); d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED - // handling device_one_time_keys_count - //if (!d->encryptionManager) - //{ - // qCDebug(E2EE) << "Encryption manager is not there yet, updating " - // "one-time key counts will be skipped"; - // return; - //} - //if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); - // !deviceOneTimeKeysCount.isEmpty()) - // d->encryptionManager->updateOneTimeKeyCounts(this, - // deviceOneTimeKeysCount); + if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys()) { + d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() - data.deviceOneTimeKeysCount()["signed_curve25519"]); + auto keys = d->olmAccount->oneTimeKeys(); + auto job = d->olmAccount->createUploadKeyRequest(keys); + run(job, ForegroundRequest); + connect(job, &BaseJob::success, this, [=](){ + d->olmAccount->markKeysAsPublished(); + }); + } #endif // Quotient_E2EE_ENABLED } diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 9368de4f..8cf21045 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -276,6 +276,11 @@ std::variant, QOlmError> QOlmAccount::createOutboun return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); } +void QOlmAccount::markKeysAsPublished() +{ + olm_account_mark_keys_as_published(m_account); +} + bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, const QString &deviceId, const QString &userId) diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index f3ca82f0..54d8506c 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -93,6 +93,8 @@ public: /// identity and one time key. std::variant, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + void markKeysAsPublished(); + // HACK do not use directly QOlmAccount(OlmAccount *account); OlmAccount *data(); -- cgit v1.2.3 From 1186c9fc980f9659191df4b3f5b540befe946dfa Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 24 May 2021 18:01:27 +0200 Subject: Make sure that only one upload is running --- lib/connection.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 9883b8f3..b91a1a90 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -110,6 +110,7 @@ public: #ifdef Quotient_E2EE_ENABLED std::unique_ptr olmAccount; //QScopedPointer encryptionManager; + bool isUploadingKeys = false; #endif // Quotient_E2EE_ENABLED QPointer resolverJob = nullptr; @@ -620,7 +621,8 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->consumePresenceData(data.takePresenceData()); d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED - if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys()) { + if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) { + d->isUploadingKeys = true; d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() - data.deviceOneTimeKeysCount()["signed_curve25519"]); auto keys = d->olmAccount->oneTimeKeys(); auto job = d->olmAccount->createUploadKeyRequest(keys); @@ -628,6 +630,9 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) connect(job, &BaseJob::success, this, [=](){ d->olmAccount->markKeysAsPublished(); }); + connect(job, &BaseJob::result, this, [=](){ + d->isUploadingKeys = false; + }); } #endif // Quotient_E2EE_ENABLED } -- cgit v1.2.3 From 211d0c1b96c13f949f50799f5a4412ae31586546 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 24 May 2021 00:40:30 +0200 Subject: Uncomment some stuff --- lib/connection.cpp | 17 ++++------------- lib/crypto/qolmsession.cpp | 5 ++--- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b91a1a90..0c0bada6 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -109,8 +109,8 @@ public: #ifdef Quotient_E2EE_ENABLED std::unique_ptr olmAccount; - //QScopedPointer encryptionManager; bool isUploadingKeys = false; + QScopedPointer encryptionManager; #endif // Quotient_E2EE_ENABLED QPointer resolverJob = nullptr; @@ -187,7 +187,6 @@ public: { qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; - /* #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; @@ -247,7 +246,6 @@ public: return std::move(decryptedEvent); #endif // Quotient_E2EE_ENABLED -*/ } }; @@ -761,7 +759,6 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { -/* #ifdef Quotient_E2EE_ENABLED // handling m.room_key to-device encrypted event visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) { @@ -771,20 +768,15 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) return; } - // TODO: full maintaining of the device keys - // with device_lists sync extention and /keys/query - qCDebug(E2EE) << "Getting device keys for the m.room_key sender:" - << ee.senderId(); - // encryptionManager->updateDeviceKeys(); - visit(*sessionDecryptMessage(ee), [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); - else + } else { qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); + } }, [](const Event& evt) { qCDebug(E2EE) << "Skipping encrypted to_device event, type" @@ -792,7 +784,6 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) }); }); #endif -*/ } void Connection::stopSync() diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp index a1f6ab71..2068a7d9 100644 --- a/lib/crypto/qolmsession.cpp +++ b/lib/crypto/qolmsession.cpp @@ -30,8 +30,7 @@ OlmSession* QOlmSession::create() std::variant, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) { if (preKeyMessage.type() != QOlmMessage::PreKey) { - qCDebug(E2EE) << "The message is not a pre-key"; - throw BadMessageFormat; + qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; } const auto olmSession = create(); @@ -48,7 +47,7 @@ std::variant, QOlmError> QOlmSession::createInbound if (error == olm_error()) { const auto lastErr = lastError(olmSession); if (lastErr == QOlmError::NotEnoughRandom) { - throw lastErr; + qCCritical(E2EE) << "Error when creating inbound session" << lastErr; } return lastErr; } -- cgit v1.2.3 From 1d851e7b2e5e0c937413b8fd4bcdb35c8492430b Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 24 May 2021 01:51:15 +0200 Subject: Upload device keys when creating a new olm account --- lib/connection.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0c0bada6..1485a347 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -455,6 +455,10 @@ void Connection::Private::completeSetup(const QString& mxId) olmAccount->createNewAccount(); accountSettings.setEncryptionAccountPickle(std::get(olmAccount->pickle(Unencrypted{}))); // TODO handle pickle errors + auto job = q->callApi(olmAccount->deviceKeys()); + connect(job, &BaseJob::failure, q, [=]{ + qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); + }); } else { // account already existing auto pickle = accountSettings.encryptionAccountPickle(); -- cgit v1.2.3 From 00b6103ceeed63238cb8c691163ed8489dd72ddb Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 15 May 2021 22:44:42 +0200 Subject: Start implementing device key tracking --- lib/connection.cpp | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/syncdata.cpp | 30 ++++++++++++++++++++++ lib/syncdata.h | 23 +++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 1485a347..66590bd8 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -102,6 +102,13 @@ public: QMetaObject::Connection syncLoopConnection {}; int syncTimeout = -1; +#ifdef Quotient_E2EE_ENABLED + QSet trackedUsers; + QSet outdatedUsers; + QHash> deviceKeys; + QueryKeysJob *currentQueryKeysJob = nullptr; +#endif + GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; @@ -153,6 +160,7 @@ public: void consumeAccountData(Events&& accountDataEvents); void consumePresenceData(Events&& presenceData); void consumeToDeviceEvents(Events&& toDeviceEvents); + void consumeDevicesList(DevicesList&& devicesList); template EventT* unpackAccountData() const @@ -247,6 +255,10 @@ public: return std::move(decryptedEvent); #endif // Quotient_E2EE_ENABLED } +#ifdef Quotient_E2EE_ENABLED + void loadOutdatedUserDevices(); + void createDevicesList(); +#endif }; Connection::Connection(const QUrl& server, QObject* parent) @@ -468,6 +480,11 @@ void Connection::Private::completeSetup(const QString& mxId) emit q->stateChanged(); emit q->connected(); q->reloadCapabilities(); +#ifdef Quotient_E2EE_ENABLED + connectSingleShot(q, &Connection::syncDone, q, [=](){ + createDevicesList(); + }); +#endif } void Connection::Private::checkAndConnect(const QString& userId, @@ -637,6 +654,7 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) }); } #endif // Quotient_E2EE_ENABLED + d->consumeDevicesList(data.takeDevicesList()); } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, @@ -790,6 +808,21 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) #endif } +void Connection::Private::consumeDevicesList(DevicesList&& devicesList) +{ +#ifdef Quotient_E2EE_ENABLED + for(const auto &changed : devicesList.changed) { + outdatedUsers += changed; + } + for(const auto &left : devicesList.left) { + trackedUsers -= left; + outdatedUsers -= left; + deviceKeys.remove(left); + } + loadOutdatedUserDevices(); +#endif +} + void Connection::stopSync() { // If there's a sync loop, break it @@ -1784,3 +1817,43 @@ QVector Connection::availableRoomVersions() co } return result; } + +#ifdef Quotient_E2EE_ENABLED +void Connection::Private::createDevicesList() +{ + for(const auto &room : q->allRooms()) { + if(!room->usesEncryption()) { + continue; + } + for(const auto &user : room->users()) { + if(user->id() != q->userId()) { + trackedUsers += user->id(); + } + } + } + outdatedUsers += trackedUsers; + loadOutdatedUserDevices(); +} + +void Connection::Private::loadOutdatedUserDevices() +{ + QHash users; + for(const auto &user : outdatedUsers) { + users[user] += QStringList(); + } + if(currentQueryKeysJob) { + currentQueryKeysJob->abandon(); + currentQueryKeysJob = nullptr; + } + auto queryKeysJob = q->callApi(users); + currentQueryKeysJob = queryKeysJob; + connect(queryKeysJob, &BaseJob::success, q, [=](){ + const auto data = queryKeysJob->deviceKeys(); + for(const auto &[user, keys] : asKeyValueRange(data)) { + //TODO Check key signature + deviceKeys[user] = keys; + outdatedUsers -= user; + } + }); +} +#endif diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 396e77eb..9c54888c 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -99,6 +99,34 @@ SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState, fromJson(unreadJson.value(HighlightCountKey), highlightCount); } +QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!devicesList.changed.isEmpty()) + sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", ")); + if (!devicesList.left.isEmpty()) + sl << QStringLiteral("left %1").arg(devicesList.left.join(", ")); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter::dumpTo(QJsonObject& jo, + const DevicesList& rs) +{ + addParam(jo, QStringLiteral("changed"), + rs.changed); + addParam(jo, QStringLiteral("left"), + rs.left); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, + DevicesList& rs) +{ + fromJson(jo["changed"_ls], rs.changed); + fromJson(jo["left"_ls], rs.left); +} + SyncData::SyncData(const QString& cacheFileName) { QFileInfo cacheFileInfo { cacheFileName }; @@ -133,6 +161,8 @@ std::pair SyncData::cacheVersion() return { MajorCacheVersion, 2 }; } +DevicesList&& SyncData::takeDevicesList() { return std::move(devicesList); } + QJsonObject SyncData::loadJson(const QString& fileName) { QFile roomFile { fileName }; diff --git a/lib/syncdata.h b/lib/syncdata.h index 36d2e0bf..7fa77eda 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -41,6 +41,27 @@ struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, RoomSummary& rs); }; +/// Information on e2e device updates. Note: only present on an +/// incremental sync. +struct DevicesList { + /// List of users who have updated their device identity keys, or who + /// now share an encrypted room with the client since the previous + /// sync response. + QStringList changed; + + /// List of users with whom we do not share any encrypted rooms + /// anymore since the previous sync response. + QStringList left; +}; + +QDebug operator<<(QDebug dhg, const DevicesList &devicesList); + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject &jo, const DevicesList &dev); + static void fillFrom(const QJsonObject& jo, DevicesList& rs); +}; + class SyncRoomData { public: QString roomId; @@ -85,6 +106,7 @@ public: return deviceOneTimeKeysCount_; } SyncDataList&& takeRoomData(); + DevicesList&& takeDevicesList(); QString nextBatch() const { return nextBatch_; } @@ -102,6 +124,7 @@ private: SyncDataList roomData; QStringList unresolvedRoomIds; QHash deviceOneTimeKeysCount_; + DevicesList devicesList; static QJsonObject loadJson(const QString& fileName); }; -- cgit v1.2.3 From 1f2ae094b29aa3fc237919659ced66c6d236d068 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 16 May 2021 19:58:00 +0200 Subject: Clear current query job when it finished --- lib/connection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 66590bd8..41dd71f6 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1848,6 +1848,7 @@ void Connection::Private::loadOutdatedUserDevices() auto queryKeysJob = q->callApi(users); currentQueryKeysJob = queryKeysJob; connect(queryKeysJob, &BaseJob::success, q, [=](){ + currentQueryKeysJob = nullptr; const auto data = queryKeysJob->deviceKeys(); for(const auto &[user, keys] : asKeyValueRange(data)) { //TODO Check key signature -- cgit v1.2.3 From e0945db3c4c539040f07ff7683efa9dc4e6b9e6a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 18 May 2021 21:44:09 +0200 Subject: Actually load devices from sync data and filter relevant users --- lib/connection.cpp | 8 ++++++-- lib/syncdata.cpp | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 41dd71f6..8ceb2a44 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -812,14 +812,18 @@ void Connection::Private::consumeDevicesList(DevicesList&& devicesList) { #ifdef Quotient_E2EE_ENABLED for(const auto &changed : devicesList.changed) { - outdatedUsers += changed; + if(trackedUsers.contains(changed)) { + outdatedUsers += changed; + } } for(const auto &left : devicesList.left) { trackedUsers -= left; outdatedUsers -= left; deviceKeys.remove(left); } - loadOutdatedUserDevices(); + if(!outdatedUsers.isEmpty()) { + loadOutdatedUserDevices(); + } #endif } diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 9c54888c..d0533fc9 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -205,6 +205,10 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) fromJson(json.value("device_one_time_keys_count"_ls), deviceOneTimeKeysCount_); + if(json.contains("device_lists")) { + fromJson(json.value("device_lists"), devicesList); + } + auto rooms = json.value("rooms"_ls).toObject(); auto totalRooms = 0; auto totalEvents = 0; -- 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/connection.cpp | 13 +++++++++++++ lib/room.cpp | 3 +++ 2 files changed, 16 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 8ceb2a44..2864f3b3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1861,4 +1861,17 @@ void Connection::Private::loadOutdatedUserDevices() } }); } + +void Connection::newEncryptedRoom(Room *room) +{ + for(const auto &user : room->users()) { + if(!d->trackedUsers.contains(user->id())) { + d->trackedUsers += user->id(); + d->outdatedUsers += user->id(); + } + } + if(!d->outdatedUsers.isEmpty()) { + d->loadOutdatedUserDevices(); + } +} #endif 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/connection.cpp | 2 +- lib/room.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2864f3b3..b87610b7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1862,7 +1862,7 @@ void Connection::Private::loadOutdatedUserDevices() }); } -void Connection::newEncryptedRoom(Room *room) +void Connection::encryptionUpdate(Room *room) { for(const auto &user : room->users()) { if(!d->trackedUsers.contains(user->id())) { 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 6449f66152396ed539904b0e89d41601aeadf30d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 19 May 2021 23:23:46 +0200 Subject: Verify deviceKeys signatures --- lib/connection.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b87610b7..06b9bcbc 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1855,8 +1855,22 @@ void Connection::Private::loadOutdatedUserDevices() currentQueryKeysJob = nullptr; const auto data = queryKeysJob->deviceKeys(); for(const auto &[user, keys] : asKeyValueRange(data)) { - //TODO Check key signature - deviceKeys[user] = keys; + deviceKeys[user].clear(); + for(const auto &device : keys) { + if(device.userId != user) { + qCWarning(E2EE) << "mxId mismatch during device key verification:" << device.userId << user; + continue; + } + if(!device.algorithms.contains("m.olm.v1.curve25519-aes-sha2") || !device.algorithms.contains("m.megolm.v1.aes-sha2")) { + qCWarning(E2EE) << "Unsupported encryption algorithms found" << device.algorithms; + continue; + } + if(verifyIdentitySignature(device, device.deviceId, device.userId)) { + qCWarning(E2EE) << "Failed to verify devicekeys signature. Skipping this device"; + continue; + } + deviceKeys[user][device.deviceId] = device; + } outdatedUsers -= user; } }); -- cgit v1.2.3 From 5d3fe54fdfae4b74272a80c4bbe8f5d8a3e4c5cb Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 20 May 2021 15:44:02 +0200 Subject: Fix signature verification logic --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 06b9bcbc..6facd316 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1865,7 +1865,7 @@ void Connection::Private::loadOutdatedUserDevices() qCWarning(E2EE) << "Unsupported encryption algorithms found" << device.algorithms; continue; } - if(verifyIdentitySignature(device, device.deviceId, device.userId)) { + if(!verifyIdentitySignature(device, device.deviceId, device.userId)) { qCWarning(E2EE) << "Failed to verify devicekeys signature. Skipping this device"; continue; } -- cgit v1.2.3 From 265f105d77bf91c127c363b0c880357f91df7db4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 21 May 2021 00:29:08 +0200 Subject: Cache deviceslist across restarts --- lib/connection.cpp | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++- lib/csapi/keys.h | 5 +++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 6facd316..e7a26f4b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -258,6 +258,8 @@ public: #ifdef Quotient_E2EE_ENABLED void loadOutdatedUserDevices(); void createDevicesList(); + void saveDevicesList(); + void loadDevicesList(); #endif }; @@ -482,7 +484,7 @@ void Connection::Private::completeSetup(const QString& mxId) q->reloadCapabilities(); #ifdef Quotient_E2EE_ENABLED connectSingleShot(q, &Connection::syncDone, q, [=](){ - createDevicesList(); + loadDevicesList(); }); #endif } @@ -1873,6 +1875,7 @@ void Connection::Private::loadOutdatedUserDevices() } outdatedUsers -= user; } + saveDevicesList(); }); } @@ -1888,4 +1891,98 @@ void Connection::encryptionUpdate(Room *room) d->loadOutdatedUserDevices(); } } + +void Connection::Private::saveDevicesList() +{ + if (!cacheState) + return; + + QElapsedTimer et; + et.start(); + + QFile outFile { q->stateCacheDir().filePath("deviceslist.json") }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); + qCWarning(MAIN) << "Caching the rooms state disabled"; + cacheState = false; + return; + } + + QJsonObject rootObj { + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), SyncData::cacheVersion().first }, + { QStringLiteral("minor"), SyncData::cacheVersion().second } } } + }; + { + QJsonObject trackedUsersJson; + QJsonObject outdatedUsersJson; + for (const auto &user : trackedUsers) { + trackedUsersJson.insert(user, QJsonValue::Null); + } + for (const auto &user : outdatedUsers) { + outdatedUsersJson.insert(user, QJsonValue::Null); + } + rootObj.insert(QStringLiteral("tracked_users"), trackedUsersJson); + rootObj.insert(QStringLiteral("outdated_users"), outdatedUsersJson); + QJsonObject devicesList = toJson>>(deviceKeys); + rootObj.insert(QStringLiteral("devices_list"), devicesList); + rootObj.insert(QStringLiteral("sync_token"), q->nextBatchToken()); + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = + cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() + : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); +#else + QJsonDocument json { rootObj }; + const auto data = cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); +#endif + qCDebug(PROFILER) << "DeviceList generated in" << et; + + outFile.write(data.data(), data.size()); + qCDebug(E2EE) << "DevicesList saved to" << outFile.fileName(); +} + +void Connection::Private::loadDevicesList() +{ + QFile file { q->stateCacheDir().filePath("deviceslist.json") }; + if(!file.exists() || !file.open(QIODevice::ReadOnly)) { + qCDebug(E2EE) << "No devicesList cache exists. Creating new"; + createDevicesList(); + 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) << "DevicesList cache is broken or empty, discarding"; + createDevicesList(); + return; + } + for(const auto &user : json["tracked_users"].toArray()) { + trackedUsers += user.toString(); + } + for(const auto &user : json["outdated_users"].toArray()) { + outdatedUsers += user.toString(); + } + + deviceKeys = fromJson>>(json["devices_list"].toObject()); + auto oldToken = json["sync_token"].toString(); + auto changesJob = q->callApi(oldToken, q->nextBatchToken()); + connect(changesJob, &BaseJob::success, q, [=](){ + for(const auto &user : changesJob->changed()) { + outdatedUsers += user; + } + loadOutdatedUserDevices(); + }); +} #endif diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 7db09e8d..b1cc640c 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -166,6 +166,11 @@ struct JsonObjectConverter { fillFromJson(jo, result); fromJson(jo.value("unsigned"_ls), result.unsignedData); } + static void dumpTo(QJsonObject& jo, const QueryKeysJob::DeviceInformation& deviceInformation) + { + jo = toJson(deviceInformation); + //addParam<>(jo, "unsigned"_ls, deviceInformation.unsignedData); + } }; /*! \brief Claim one-time encryption keys. -- cgit v1.2.3 From 5f3e33e1c15be19f09d83a0d6f44d551021a9d44 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 5 Feb 2021 18:45:30 +0100 Subject: Implement key verification events --- CMakeLists.txt | 1 + lib/events/keyverificationevent.cpp | 193 ++++++++++++++++++++++++++++++++++++ lib/events/keyverificationevent.h | 167 +++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 lib/events/keyverificationevent.cpp create mode 100644 lib/events/keyverificationevent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5601a281..92a9b213 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,6 +153,7 @@ list(APPEND lib_SRCS lib/events/encryptedevent.cpp lib/events/roomkeyevent.cpp lib/events/stickerevent.cpp + lib/events/keyverificationevent.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp lib/jobs/syncjob.cpp diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp new file mode 100644 index 00000000..938b3bde --- /dev/null +++ b/lib/events/keyverificationevent.cpp @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "keyverificationevent.h" + +using namespace Quotient; + +KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationRequestEvent::fromDevice() const +{ + return contentJson()["from_device"_ls].toString(); +} + +QString KeyVerificationRequestEvent::transactionId() const +{ + return contentJson()["transaction_id"_ls].toString(); +} + +QStringList KeyVerificationRequestEvent::methods() const +{ + QStringList methods; + for (const auto &method : contentJson()["methods"].toArray()) { + methods.append(method.toString()); + } + return methods; +} + +uint64_t KeyVerificationRequestEvent::timestamp() const +{ + return contentJson()["timestamp"_ls].toDouble(); +} + +KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationStartEvent::fromDevice() const +{ + return contentJson()["from_device"_ls].toString(); +} + +QString KeyVerificationStartEvent::transactionId() const +{ + return contentJson()["transaction_id"_ls].toString(); +} + +QString KeyVerificationStartEvent::method() const +{ + return contentJson()["method"_ls].toString(); +} + +Omittable KeyVerificationStartEvent::nextMethod() const +{ + auto next = contentJson()["method"_ls]; + if (next.isUndefined()) { + return std::nullopt; + } + return next.toString(); +} + +QStringList KeyVerificationStartEvent::keyAgreementProtocols() const +{ + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + QStringList protocols; + for (const auto &proto : contentJson()["key_agreement_protocols"_ls].toArray()) { + protocols.append(proto.toString()); + } + return protocols; +} + +QStringList KeyVerificationStartEvent::hashes() const +{ + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + QStringList hashes; + for (const auto &hashItem : contentJson()["hashes"_ls].toArray()) { + hashes.append(hashItem.toString()); + } + return hashes; +} + +QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const +{ + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + + QStringList codes; + for (const auto &code : contentJson()["message_authentication_codes"_ls].toArray()) { + codes.append(code.toString()); + } + return codes; +} + +QString KeyVerificationStartEvent::shortAuthenticationString() const +{ + return contentJson()["short_authentification_string"_ls].toString(); +} + +KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationAcceptEvent::transactionId() const +{ + return contentJson()["transaction_id"_ls].toString(); +} + +QString KeyVerificationAcceptEvent::method() const +{ + return contentJson()["method"_ls].toString(); +} + +QString KeyVerificationAcceptEvent::keyAgreementProtocol() const +{ + return contentJson()["key_agreement_protocol"_ls].toString(); +} + +QString KeyVerificationAcceptEvent::hashData() const +{ + return contentJson()["hash"_ls].toString(); +} + +QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const +{ + QStringList strings; + for (const auto &authenticationString : contentJson()["short_authentification_string"].toArray()) { + strings.append(authenticationString.toString()); + } + return strings; +} + +QString KeyVerificationAcceptEvent::commitement() const +{ + return contentJson()["commitement"].toString(); +} + +KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationCancelEvent::transactionId() const +{ + return contentJson()["transaction_id"_ls].toString(); +} + +QString KeyVerificationCancelEvent::reason() const +{ + return contentJson()["reason"_ls].toString(); +} + +QString KeyVerificationCancelEvent::code() const +{ + return contentJson()["code"_ls].toString(); +} + +KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationKeyEvent::transactionId() const +{ + return contentJson()["transaction_id"_ls].toString(); +} + +QString KeyVerificationKeyEvent::key() const +{ + return contentJson()["key"_ls].toString(); +} + +KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationMacEvent::transactionId() const +{ + return contentJson()["transaction_id"].toString(); +} + +QString KeyVerificationMacEvent::keys() const +{ + return contentJson()["keys"].toString(); +} + +QHash KeyVerificationMacEvent::mac() const +{ + QHash macs; + const auto macObj = contentJson()["mac"_ls].toObject(); + for (auto mac = macObj.constBegin(); mac != macObj.constEnd(); mac++) { + macs.insert(mac.key(), mac.value().toString()); + } + return macs; +} diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h new file mode 100644 index 00000000..13e7dcdd --- /dev/null +++ b/lib/events/keyverificationevent.h @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "event.h" + +namespace Quotient { + +/// Requests a key verification with another user's devices. +/// Typically sent as a to-device event. +class KeyVerificationRequestEvent : public Event { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent) + + explicit KeyVerificationRequestEvent(const QJsonObject& obj); + + /// The device ID which is initiating the request. + QString fromDevice() const; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QString transactionId() const; + + /// The verification methods supported by the sender. + QStringList methods() const; + + /// The POSIX timestamp in milliseconds for when the request was + /// made. If the request is in the future by more than 5 minutes or + /// more than 10 minutes in the past, the message should be ignored + /// by the receiver. + uint64_t timestamp() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) + +/// Begins a key verification process. +class KeyVerificationStartEvent : public Event { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) + + explicit KeyVerificationStartEvent(const QJsonObject &obj); + + /// The device ID which is initiating the process. + QString fromDevice() const; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QString transactionId() const; + + /// The verification method to use. + QString method() const; + + /// Optional method to use to verify the other user's key with. + Omittable nextMethod() const; + + // SAS.V1 methods + + /// The key agreement protocols the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList keyAgreementProtocols() const; + + /// The hash methods the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList hashes() const; + + /// The message authentication codes that the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList messageAuthenticationCodes() const; + + /// The SAS methods the sending device (and the sending device's + /// user) understands. + /// \note Only exist if method is m.sas.v1 + QString shortAuthenticationString() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationStartEvent) + +/// Accepts a previously sent m.key.verification.start message. +/// Typically sent as a to-device event. +class KeyVerificationAcceptEvent : public Event { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent) + + explicit KeyVerificationAcceptEvent(const QJsonObject& obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// The verification method to use. Must be 'm.sas.v1'. + QString method() const; + + /// The key agreement protocol the device is choosing to use, out of + /// the options in the m.key.verification.start message. + QString keyAgreementProtocol() const; + + /// The hash method the device is choosing to use, out of the + /// options in the m.key.verification.start message. + QString hashData() const; + + /// The message authentication code the device is choosing to use, out + /// of the options in the m.key.verification.start message. + QString messageAuthenticationCode() const; + + /// The SAS methods both devices involved in the verification process understand. + QStringList shortAuthenticationString() const; + + /// The hash (encoded as unpadded base64) of the concatenation of the + /// device's ephemeral public key (encoded as unpadded base64) and the + /// canonical JSON representation of the m.key.verification.start message. + QString commitement() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) + +class KeyVerificationCancelEvent : public Event { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) + + explicit KeyVerificationCancelEvent(const QJsonObject &obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// A human readable description of the code. The client should only + /// rely on this string if it does not understand the code. + QString reason() const; + + /// The error code for why the process/request was cancelled by the user. + QString code() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) + +/// Sends the ephemeral public key for a device to the partner device. +/// Typically sent as a to-device event. +class KeyVerificationKeyEvent : public Event { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) + + explicit KeyVerificationKeyEvent(const QJsonObject &obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// The device's ephemeral public key, encoded as unpadded base64. + QString key() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationKeyEvent) + +/// Sends the MAC of a device's key to the partner device. +class KeyVerificationMacEvent : public Event { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) + + explicit KeyVerificationMacEvent(const QJsonObject &obj); + + /// An opaque identifier for the verification process. + QString transactionId() const; + + /// The device's ephemeral public key, encoded as unpadded base64. + QString keys() const; + + QHash mac() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationMacEvent) +} // namespace Quotient -- cgit v1.2.3 From a2b65a3abb635a478555b61de33cb5257d8dd34e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Jun 2021 21:38:26 +0200 Subject: Query for keys less and actually load users from cache --- lib/connection.cpp | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index e7a26f4b..35b2f7fc 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -482,11 +482,6 @@ void Connection::Private::completeSetup(const QString& mxId) emit q->stateChanged(); emit q->connected(); q->reloadCapabilities(); -#ifdef Quotient_E2EE_ENABLED - connectSingleShot(q, &Connection::syncDone, q, [=](){ - loadDevicesList(); - }); -#endif } void Connection::Private::checkAndConnect(const QString& userId, @@ -636,11 +631,6 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { - d->data->setLastEvent(data.nextBatch()); - d->consumeRoomData(data.takeRoomData(), fromCache); - d->consumeAccountData(data.takeAccountData()); - d->consumePresenceData(data.takePresenceData()); - d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) { d->isUploadingKeys = true; @@ -655,8 +645,19 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->isUploadingKeys = false; }); } -#endif // Quotient_E2EE_ENABLED + static bool first = true; + if(first) { + d->loadDevicesList(); + first = false; + } + d->consumeDevicesList(data.takeDevicesList()); +#endif // Quotient_E2EE_ENABLED + d->data->setLastEvent(data.nextBatch()); + d->consumeRoomData(data.takeRoomData(), fromCache); + d->consumeAccountData(data.takeAccountData()); + d->consumePresenceData(data.takePresenceData()); + d->consumeToDeviceEvents(data.takeToDeviceEvents()); } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, @@ -813,9 +814,11 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) void Connection::Private::consumeDevicesList(DevicesList&& devicesList) { #ifdef Quotient_E2EE_ENABLED + bool hasNewOutdatedUser = false; for(const auto &changed : devicesList.changed) { if(trackedUsers.contains(changed)) { outdatedUsers += changed; + hasNewOutdatedUser = true; } } for(const auto &left : devicesList.left) { @@ -823,7 +826,7 @@ void Connection::Private::consumeDevicesList(DevicesList&& devicesList) outdatedUsers -= left; deviceKeys.remove(left); } - if(!outdatedUsers.isEmpty()) { + if(hasNewOutdatedUser) { loadOutdatedUserDevices(); } #endif @@ -1881,13 +1884,15 @@ void Connection::Private::loadOutdatedUserDevices() void Connection::encryptionUpdate(Room *room) { + bool hasNewOutdatedUser = false; for(const auto &user : room->users()) { if(!d->trackedUsers.contains(user->id())) { d->trackedUsers += user->id(); d->outdatedUsers += user->id(); + hasNewOutdatedUser = true; } } - if(!d->outdatedUsers.isEmpty()) { + if(hasNewOutdatedUser) { d->loadOutdatedUserDevices(); } } @@ -1916,13 +1921,13 @@ void Connection::Private::saveDevicesList() { QStringLiteral("minor"), SyncData::cacheVersion().second } } } }; { - QJsonObject trackedUsersJson; - QJsonObject outdatedUsersJson; + QJsonArray trackedUsersJson; + QJsonArray outdatedUsersJson; for (const auto &user : trackedUsers) { - trackedUsersJson.insert(user, QJsonValue::Null); + trackedUsersJson += user; } for (const auto &user : outdatedUsers) { - outdatedUsersJson.insert(user, QJsonValue::Null); + outdatedUsersJson += user; } rootObj.insert(QStringLiteral("tracked_users"), trackedUsersJson); rootObj.insert(QStringLiteral("outdated_users"), outdatedUsersJson); @@ -1979,10 +1984,14 @@ void Connection::Private::loadDevicesList() auto oldToken = json["sync_token"].toString(); auto changesJob = q->callApi(oldToken, q->nextBatchToken()); connect(changesJob, &BaseJob::success, q, [=](){ + bool hasNewOutdatedUser = false; for(const auto &user : changesJob->changed()) { outdatedUsers += user; + hasNewOutdatedUser = true; + } + if(hasNewOutdatedUser) { + loadOutdatedUserDevices(); } - loadOutdatedUserDevices(); }); } #endif -- cgit v1.2.3 From d997bbf54e755c42b62fadca8ee63b27aa0e7480 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 9 Jun 2021 15:59:10 +0200 Subject: More fixes --- lib/connection.cpp | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 35b2f7fc..9ea09258 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -107,6 +107,7 @@ public: QSet outdatedUsers; QHash> deviceKeys; QueryKeysJob *currentQueryKeysJob = nullptr; + bool encryptionUpdateRequired = false; #endif GetCapabilitiesJob* capabilitiesJob = nullptr; @@ -257,7 +258,6 @@ public: } #ifdef Quotient_E2EE_ENABLED void loadOutdatedUserDevices(); - void createDevicesList(); void saveDevicesList(); void loadDevicesList(); #endif @@ -658,6 +658,10 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->consumeAccountData(data.takeAccountData()); d->consumePresenceData(data.takePresenceData()); d->consumeToDeviceEvents(data.takeToDeviceEvents()); + if(d->encryptionUpdateRequired) { + d->loadOutdatedUserDevices(); + d->encryptionUpdateRequired = false; + } } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, @@ -1828,22 +1832,6 @@ QVector Connection::availableRoomVersions() co } #ifdef Quotient_E2EE_ENABLED -void Connection::Private::createDevicesList() -{ - for(const auto &room : q->allRooms()) { - if(!room->usesEncryption()) { - continue; - } - for(const auto &user : room->users()) { - if(user->id() != q->userId()) { - trackedUsers += user->id(); - } - } - } - outdatedUsers += trackedUsers; - loadOutdatedUserDevices(); -} - void Connection::Private::loadOutdatedUserDevices() { QHash users; @@ -1884,17 +1872,13 @@ void Connection::Private::loadOutdatedUserDevices() void Connection::encryptionUpdate(Room *room) { - bool hasNewOutdatedUser = false; for(const auto &user : room->users()) { if(!d->trackedUsers.contains(user->id())) { d->trackedUsers += user->id(); d->outdatedUsers += user->id(); - hasNewOutdatedUser = true; + d->encryptionUpdateRequired = true; } } - if(hasNewOutdatedUser) { - d->loadOutdatedUserDevices(); - } } void Connection::Private::saveDevicesList() @@ -1956,7 +1940,6 @@ void Connection::Private::loadDevicesList() QFile file { q->stateCacheDir().filePath("deviceslist.json") }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No devicesList cache exists. Creating new"; - createDevicesList(); return; } auto data = file.readAll(); @@ -1970,7 +1953,6 @@ void Connection::Private::loadDevicesList() ; if (json.isEmpty()) { qCWarning(MAIN) << "DevicesList cache is broken or empty, discarding"; - createDevicesList(); return; } for(const auto &user : json["tracked_users"].toArray()) { -- cgit v1.2.3 From 8b573fc9f9f4b65ace0fdc5b4598f1974e01f7e3 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 9 Jun 2021 16:05:16 +0200 Subject: Fix compilation without E2EE --- lib/connection.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 9ea09258..10256d9c 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -658,10 +658,12 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->consumeAccountData(data.takeAccountData()); d->consumePresenceData(data.takePresenceData()); d->consumeToDeviceEvents(data.takeToDeviceEvents()); +#ifdef Quotient_E2EE_ENABLED if(d->encryptionUpdateRequired) { d->loadOutdatedUserDevices(); d->encryptionUpdateRequired = false; } +#endif } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, -- cgit v1.2.3 From cddab50ca80944203930255e37e825abb47a272b Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 10 Jun 2021 15:22:15 +0200 Subject: Finish writing TestOlmAccount::signatureValid --- autotests/testolmaccount.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 1c296db9..1bd63a48 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -4,6 +4,7 @@ #include "testolmaccount.h" #include "crypto/qolmaccount.h" +#include "crypto/qolmutility.h" #include "connection.h" #include "events/encryptedfile.h" #include "networkaccessmanager.h" @@ -47,12 +48,12 @@ void TestOlmAccount::signatureValid() const auto signature = olmAccount.sign(message); QVERIFY(QByteArray::fromBase64Encoding(signature).decodingStatus == QByteArray::Base64DecodingStatus::Ok); - //let utility = OlmUtility::new(); - //let identity_keys = olm_account.parsed_identity_keys(); - //let ed25519_key = identity_keys.ed25519(); - //assert!(utility - // .ed25519_verify(&ed25519_key, message, &signature) - // .unwrap()); + QOlmUtility utility; + const auto identityKeys = olmAccount.identityKeys(); + const auto ed25519Key = identityKeys.ed25519; + const auto verify = utility.ed25519Verify(ed25519Key, message, signature); + QVERIFY(std::holds_alternative(verify)); + QVERIFY(std::get(verify) == true); } void TestOlmAccount::oneTimeKeysValid() @@ -341,7 +342,7 @@ void TestOlmAccount::claimKeys() QVariantMap varMap = oneTimeKey.toMap(); bool found = false; - for (const auto key : varMap.keys()) { + for (const auto &key : varMap.keys()) { if (key.startsWith(QStringLiteral("signed_curve25519"))) { found = true; } -- cgit v1.2.3 From 9bf12da8aaa1b2005d9d7d8eae4269c8a9bf1c08 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 10 Jun 2021 17:42:07 +0200 Subject: test: Add QueryKey test (failing) --- autotests/testolmaccount.cpp | 89 ++++++++++++++++++++++++++++++++++++++++---- autotests/testolmaccount.h | 1 + 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 1bd63a48..9195bb62 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-FileCopyrightText: 2020 mtxclient developers // // SPDX-License-Identifier: LGPL-2.1-or-later @@ -285,6 +286,79 @@ void TestOlmAccount::uploadKeys() QVERIFY(spy3.wait(10000)); } +void TestOlmAccount::queryTest() +{ + CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + + // Create and upload keys for both users. + auto aliceOlm = alice->olmAccount(); + aliceOlm->generateOneTimeKeys(1); + auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); + connect(aliceRes, &BaseJob::result, this, [aliceRes] { + QCOMPARE(aliceRes->oneTimeKeyCounts().size(), 1); + QCOMPARE(aliceRes->oneTimeKeyCounts()["signed_curve25519"], 1); + }); + QSignalSpy spy(aliceRes, &BaseJob::result); + bob->run(aliceRes); + QVERIFY(spy.wait(10000)); + + auto bobOlm = bob->olmAccount(); + bobOlm->generateOneTimeKeys(1); + auto bobRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); + connect(bobRes, &BaseJob::result, this, [bobRes] { + QCOMPARE(bobRes->oneTimeKeyCounts().size(), 1); + QCOMPARE(bobRes->oneTimeKeyCounts()["signed_curve25519"], 1); + }); + QSignalSpy spy1(bobRes, &BaseJob::result); + bob->run(bobRes); + QVERIFY(spy1.wait(10000)); + + { + // Each user is requests each other's keys. + QHash deviceKeys; + deviceKeys[bob->userId()] = QStringList(); + auto job = alice->callApi(deviceKeys); + QSignalSpy spy(job, &BaseJob::result); + connect(job, &BaseJob::result, this, [job, &bob, &bobOlm] { + QCOMPARE(job->failures().size(), 0); + + auto aliceDevices = job->deviceKeys()[bob->userId()]; + QVERIFY(aliceDevices.size() > 0); + + auto devKeys = aliceDevices[bob->deviceId()]; + QCOMPARE(devKeys.userId, bob->userId()); + QCOMPARE(devKeys.deviceId, bob->deviceId()); + QCOMPARE(devKeys.keys, bobOlm->deviceKeys().keys); + QCOMPARE(devKeys.signatures, bobOlm->deviceKeys().signatures); + }); + QVERIFY(spy.wait(10000)); + } + + { + QHash deviceKeys; + deviceKeys[alice->userId()] = QStringList(); + auto job = bob->callApi(deviceKeys); + QSignalSpy spy(job, &BaseJob::result); + connect(job, &BaseJob::result, this, [job, &alice, &aliceOlm] { + QCOMPARE(job->failures().size(), 0); + + auto bobDevices = job->deviceKeys()[alice->userId()]; + QVERIFY(bobDevices.size() > 0); + + auto devKeys = bobDevices[alice->deviceId()]; + qDebug() << bobDevices.keys(); + QCOMPARE(devKeys.userId, alice->userId()); + QCOMPARE(devKeys.deviceId, alice->deviceId()); + QCOMPARE(devKeys.keys, aliceOlm->deviceKeys().keys); + QCOMPARE(devKeys.signatures, aliceOlm->deviceKeys().signatures); + }); + QVERIFY(spy.wait(10000)); + } +} + + + void TestOlmAccount::claimKeys() { CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") @@ -365,36 +439,37 @@ void TestOlmAccount::claimMultipleKeys() auto olm = alice->olmAccount(); olm->generateOneTimeKeys(10); auto res = olm->createUploadKeyRequest(olm->oneTimeKeys()); - alice->run(res); + QSignalSpy spy(res, &BaseJob::result); connect(res, &BaseJob::result, this, [res] { QCOMPARE(res->oneTimeKeyCounts().size(), 1); QCOMPARE(res->oneTimeKeyCounts()["signed_curve25519"], 10); }); - QSignalSpy spy(res, &BaseJob::result); + alice->run(res); auto olm1 = alice1->olmAccount(); olm1->generateOneTimeKeys(10); auto res1 = olm1->createUploadKeyRequest(olm1->oneTimeKeys()); - alice1->run(res1); + QSignalSpy spy1(res1, &BaseJob::result); connect(res1, &BaseJob::result, this, [res1] { QCOMPARE(res1->oneTimeKeyCounts().size(), 1); QCOMPARE(res1->oneTimeKeyCounts()["signed_curve25519"], 10); }); - QSignalSpy spy1(res1, &BaseJob::result); + alice1->run(res1); auto olm2 = alice2->olmAccount(); olm2->generateOneTimeKeys(10); auto res2 = olm2->createUploadKeyRequest(olm2->oneTimeKeys()); - alice2->run(res2); + QSignalSpy spy2(res2, &BaseJob::result); connect(res2, &BaseJob::result, this, [res2] { QCOMPARE(res2->oneTimeKeyCounts().size(), 1); QCOMPARE(res2->oneTimeKeyCounts()["signed_curve25519"], 10); }); - QSignalSpy spy2(res2, &BaseJob::result); + alice2->run(res2); + QVERIFY(spy.wait(10000)); QVERIFY(spy1.wait(10000)); - QVERIFY(spy2.wait(10000)); + QVERIFY(spy2.wait(1000)); // TODO this is failing even with 10000 // Bob will claim all keys from alice CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index bab9eed2..ee390613 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -25,6 +25,7 @@ private Q_SLOTS: void uploadOneTimeKeys(); void uploadSignedOneTimeKeys(); void uploadKeys(); + void queryTest(); void claimKeys(); void claimMultipleKeys(); }; -- cgit v1.2.3 From fc531b625efa3f0c0ceebed3c23a3d185d398a4d Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 10 Jun 2021 18:41:39 +0200 Subject: Fix tests --- autotests/testolmaccount.cpp | 6 +++--- lib/crypto/qolmaccount.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 9195bb62..192c97ac 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -300,13 +300,14 @@ void TestOlmAccount::queryTest() QCOMPARE(aliceRes->oneTimeKeyCounts()["signed_curve25519"], 1); }); QSignalSpy spy(aliceRes, &BaseJob::result); - bob->run(aliceRes); + alice->run(aliceRes); QVERIFY(spy.wait(10000)); auto bobOlm = bob->olmAccount(); bobOlm->generateOneTimeKeys(1); - auto bobRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); + auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); connect(bobRes, &BaseJob::result, this, [bobRes] { + QCOMPARE(bobRes->oneTimeKeyCounts().size(), 1); QCOMPARE(bobRes->oneTimeKeyCounts()["signed_curve25519"], 1); }); @@ -347,7 +348,6 @@ void TestOlmAccount::queryTest() QVERIFY(bobDevices.size() > 0); auto devKeys = bobDevices[alice->deviceId()]; - qDebug() << bobDevices.keys(); QCOMPARE(devKeys.userId, alice->userId()); QCOMPARE(devKeys.deviceId, alice->deviceId()); QCOMPARE(devKeys.keys, aliceOlm->deviceKeys().keys); diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 8cf21045..6b7bc9a9 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -253,7 +253,7 @@ UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKey auto temp = signOneTimeKeys(oneTimeKeys); QHash oneTimeKeysSigned; for (const auto &[keyId, key] : asKeyValueRange(temp)) { - oneTimeKeysSigned[keyId] = QVariant::fromValue(key); + oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key)); } return new UploadKeysJob(keys, oneTimeKeysSigned); -- cgit v1.2.3 From 8e0e3849da4f024d94c1cbfb053dfb47706e2d16 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 10 Jun 2021 19:14:54 +0200 Subject: Text: Add KeyChange test --- autotests/testolmaccount.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ autotests/testolmaccount.h | 1 + 2 files changed, 44 insertions(+) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 192c97ac..a31f0b98 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -494,4 +494,47 @@ void TestOlmAccount::claimMultipleKeys() QCOMPARE(job->oneTimeKeys()[userId].size(), 3); }); } + +void TestOlmAccount::keyChange() +{ + CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") + + auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), QStringList()); + connect(job, &BaseJob::result, this, [alice, job, this] () { + // Alice syncs to get the first next_batch token. + alice->sync(); + connect(alice.get(), &Connection::syncDone, this, [alice, this] { + const auto nextBatchToken = alice->nextBatchToken(); + + // generate keys and change existing one + auto aliceOlm = alice->olmAccount(); + aliceOlm->generateOneTimeKeys(1); + auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); + connect(aliceRes, &BaseJob::result, this, [aliceRes] { + QCOMPARE(aliceRes->oneTimeKeyCounts().size(), 1); + QCOMPARE(aliceRes->oneTimeKeyCounts()["signed_curve25519"], 1); + }); + QSignalSpy spy(aliceRes, &BaseJob::result); + + alice->run(aliceRes); + QVERIFY(spy.wait(10000)); + + // The key changes should contain her username + // because of the key uploading. + + auto changeJob = alice->callApi(nextBatchToken, ""); + connect(changeJob, &BaseJob::result, this, [&changeJob, &alice] { + QCOMPARE(changeJob->changed().size(), 1); + QCOMPARE(changeJob->left().size(), 0); + QCOMPARE(changeJob->changed()[0], alice->userId()); + }); + QSignalSpy spy2(changeJob, &BaseJob::result); + QVERIFY(spy2.wait(10000)); + }); + }); + QSignalSpy spy(job, &BaseJob::result); + QVERIFY(spy.wait(10000)); +} + + QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index ee390613..f6ad119b 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -28,4 +28,5 @@ private Q_SLOTS: void queryTest(); void claimKeys(); void claimMultipleKeys(); + void keyChange(); }; -- cgit v1.2.3 From 429dbc5670b3f9eba44221395a75221b8306c068 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 10 Jun 2021 21:47:46 +0200 Subject: Add a test (now failing) --- autotests/testolmaccount.cpp | 55 ++++++++++++++++++++++++++++++++++++++++---- autotests/testolmaccount.h | 1 + 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index a31f0b98..4ab21a8a 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -4,11 +4,20 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testolmaccount.h" -#include "crypto/qolmaccount.h" -#include "crypto/qolmutility.h" -#include "connection.h" -#include "events/encryptedfile.h" -#include "networkaccessmanager.h" +#include +#include +#include +#include +#include +#include +#include + +// for sleep +#ifdef _WIN32 +#include +#else +#include +#endif using namespace Quotient; @@ -531,10 +540,46 @@ void TestOlmAccount::keyChange() QSignalSpy spy2(changeJob, &BaseJob::result); QVERIFY(spy2.wait(10000)); }); + QSignalSpy spy2(alice.get(), &Connection::syncDone); + QVERIFY(spy2.wait(10000)); }); QSignalSpy spy(job, &BaseJob::result); QVERIFY(spy.wait(10000)); } +void TestOlmAccount::enableEncryption() +{ + CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + + QString joinedRoom; + + auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"}); + connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) { + room->activateEncryption(); // TODO we should also wait for it + joinedRoom = room->id(); + sleep(1); + auto job = bob->joinRoom(room->id()); + QSignalSpy spy(job, &BaseJob::result); + QVERIFY(spy.wait(10000)); + }); + QSignalSpy spy(job, &BaseJob::result); + QVERIFY(spy.wait(10000)); + + bob->sync(); + connect(bob.get(), &Connection::syncDone, this, [bob, &joinedRoom, this] { + auto &events = bob->room(joinedRoom)->messageEvents(); + bool hasEncryption = false; + for (auto it = events.rbegin(); it != events.rend(); ++it) { + auto event = it->event(); + if (eventCast(event)) { + hasEncryption = true; + } + } + QVERIFY(hasEncryption); + }); + QSignalSpy spy2(bob.get(), &Connection::syncDone); + QVERIFY(spy2.wait(10000)); +} QTEST_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index f6ad119b..f1f80454 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -29,4 +29,5 @@ private Q_SLOTS: void claimKeys(); void claimMultipleKeys(); void keyChange(); + void enableEncryption(); }; -- cgit v1.2.3 From d30a5153cca335ff6f23cc5a3019001913df0edd Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 10 Jun 2021 23:05:46 +0200 Subject: Fix JSON formatting for one-time-key signature creation --- lib/crypto/qolmaccount.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 6b7bc9a9..22b1faef 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -206,7 +206,7 @@ SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QStr QByteArray QOlmAccount::signOneTimeKey(const QString &key) const { QJsonDocument j(QJsonObject{{"key", key}}); - return sign(j.toJson()); + return sign(j.toJson(QJsonDocument::Compact)); } std::optional QOlmAccount::removeOneTimeKeys(const std::unique_ptr &session) const -- 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 --- autotests/testolmaccount.cpp | 17 +++++++++++------ lib/room.cpp | 1 - 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 4ab21a8a..c7edd7ad 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -519,10 +519,6 @@ void TestOlmAccount::keyChange() auto aliceOlm = alice->olmAccount(); aliceOlm->generateOneTimeKeys(1); auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); - connect(aliceRes, &BaseJob::result, this, [aliceRes] { - QCOMPARE(aliceRes->oneTimeKeyCounts().size(), 1); - QCOMPARE(aliceRes->oneTimeKeyCounts()["signed_curve25519"], 1); - }); QSignalSpy spy(aliceRes, &BaseJob::result); alice->run(aliceRes); @@ -557,12 +553,15 @@ void TestOlmAccount::enableEncryption() auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"}); connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) { room->activateEncryption(); // TODO we should also wait for it + QSignalSpy spy(room, &Room::encryption); + joinedRoom = room->id(); - sleep(1); auto job = bob->joinRoom(room->id()); - QSignalSpy spy(job, &BaseJob::result); + QSignalSpy spy1(job, &BaseJob::result); QVERIFY(spy.wait(10000)); + QVERIFY(spy1.wait(10000)); }); + QSignalSpy spy(job, &BaseJob::result); QVERIFY(spy.wait(10000)); @@ -574,8 +573,14 @@ void TestOlmAccount::enableEncryption() auto event = it->event(); if (eventCast(event)) { hasEncryption = true; + } else { + qDebug() << event->matrixType() << typeId() << event->type(); + if ( event->matrixType() == "m.room.encryption") { + qDebug() << event->contentJson(); + } } } + QVERIFY(bob->room(joinedRoom)->usesEncryption()); QVERIFY(hasEncryption); }); QSignalSpy spy2(bob.get(), &Connection::syncDone); 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 40d6616ef0c4a9be20d5fe5e50f4b9959d0ab3d1 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 11 Jun 2021 18:37:58 +0200 Subject: Cleanup and Refactor EncryptionManager --- lib/connection.cpp | 45 +++--- lib/encryptionmanager.cpp | 352 +++++++--------------------------------------- lib/encryptionmanager.h | 20 +-- 3 files changed, 69 insertions(+), 348 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 10256d9c..2d040e8a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -118,7 +118,7 @@ public: #ifdef Quotient_E2EE_ENABLED std::unique_ptr olmAccount; bool isUploadingKeys = false; - QScopedPointer encryptionManager; + EncryptionManager *encryptionManager; #endif // Quotient_E2EE_ENABLED QPointer resolverJob = nullptr; @@ -194,17 +194,14 @@ public: EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) { - qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; - return {}; #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; -#else // Quotient_E2EE_ENABLED +#else if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) return {}; - const auto identityKey = - encryptionManager->account()->identityKeys().curve25519; + const auto identityKey = olmAccount->identityKeys().curve25519; const auto personalCipherObject = encryptedEvent.ciphertext(identityKey); if (personalCipherObject.isEmpty()) { @@ -212,11 +209,11 @@ public: return {}; } const auto decrypted = encryptionManager->sessionDecryptMessage( - personalCipherObject, encryptedEvent.senderKey().toLatin1()); + personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" << encryptedEvent.senderKey() - << encryptionManager->account()->oneTimeKeys().keys; + << olmAccount->oneTimeKeys().keys; return {}; } @@ -233,22 +230,18 @@ public: // TODO: keys to constants const auto decryptedEventObject = decryptedEvent->fullJson(); - const auto recipient = - decryptedEventObject.value("recipient"_ls).toString(); + const auto recipient = decryptedEventObject.value("recipient"_ls).toString(); if (recipient != data->userId()) { qCDebug(E2EE) << "Found user" << recipient << "instead of us" << data->userId() << "in Olm plaintext"; return {}; } - const auto ourKey = - decryptedEventObject.value("recipient_keys"_ls).toObject() - .value(Ed25519Key).toString(); - if (ourKey - != QString::fromUtf8( - encryptionManager->account()->identityKeys().ed25519)) { + const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject() + .value(Ed25519Key).toString(); + if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) { qCDebug(E2EE) << "Found key" << ourKey << "instead of ours own ed25519 key" - << encryptionManager->account()->identityKeys().ed25519 + << olmAccount->identityKeys().ed25519 << "in Olm plaintext"; return {}; } @@ -266,6 +259,7 @@ public: Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent), d(new Private(std::make_unique(server))) { + d->encryptionManager = new EncryptionManager(this); d->q = this; // All d initialization should occur before this line } @@ -791,21 +785,20 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED - // handling m.room_key to-device encrypted event - visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) { - if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm" - << ee.algorithm() << "is not supported"; + qWarning() << "Consuming to device events" << toDeviceEvents.size(); + if(toDeviceEvents.size() > 0) + visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { + if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); return; } - visit(*sessionDecryptMessage(ee), - [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { + visit(*sessionDecryptMessage(event), + [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); } else { - qCDebug(E2EE) - << "Encrypted event room id" << roomKeyEvent.roomId() + qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); } }, diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 53890fdb..b9bd6646 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -26,69 +26,16 @@ using std::move; class EncryptionManager::Private { public: - explicit Private(const QByteArray& encryptionAccountPickle, - float signedKeysProportion, float oneTimeKeyThreshold) + explicit Private() : q(nullptr) - , signedKeysProportion(move(signedKeysProportion)) - , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) { - Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); - Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); - if (encryptionAccountPickle.isEmpty()) { - // new e2ee TODO: olmAccount.reset(new QOlmAccount()); - } else { - // new e2ee TODO: olmAccount.reset(new QOlmAccount(encryptionAccountPickle)); // TODO: passphrase even with qtkeychain? - } - /* - * Note about targetKeysNumber: - * - * From: https://github.com/Zil0/matrix-python-sdk/ - * File: matrix_client/crypto/olm_device.py - * - * Try to maintain half the number of one-time keys libolm can hold - * uploaded on the HS. This is because some keys will be claimed by - * peers but not used instantly, and we want them to stay in libolm, - * until the limit is reached and it starts discarding keys, starting by - * the oldest. - */ - targetKeysNumber = olmAccount->maxNumberOfOneTimeKeys() / 2; - targetOneTimeKeyCounts = { - { SignedCurve25519Key, - qRound(signedKeysProportion * targetKeysNumber) }, - { Curve25519Key, - qRound((1 - signedKeysProportion) * targetKeysNumber) } - }; - updateKeysToUpload(); } ~Private() = default; EncryptionManager* q; - UploadKeysJob* uploadIdentityKeysJob = nullptr; - UploadKeysJob* uploadOneTimeKeysInitJob = nullptr; - UploadKeysJob* uploadOneTimeKeysJob = nullptr; - QueryKeysJob* queryKeysJob = nullptr; - - std::unique_ptr olmAccount; - - float signedKeysProportion; - float oneTimeKeyThreshold; - int targetKeysNumber; - - void updateKeysToUpload(); - bool oneTimeKeyShouldUpload(); - - QHash oneTimeKeyCounts; - void setOneTimeKeyCounts(const QHash oneTimeKeyCountsNewValue) - { - oneTimeKeyCounts = oneTimeKeyCountsNewValue; - updateKeysToUpload(); - } - QHash oneTimeKeysToUploadCounts; - QHash targetOneTimeKeyCounts; - // A map from senderKey to InboundSession - QMap> sessions; // TODO: cache + std::map> sessions; // TODO: cache void updateDeviceKeys( const QHash>& deviceKeys) @@ -100,279 +47,76 @@ public: } } } - QString sessionDecrypt(const QOlmMessage& message, const QString& senderKey) + QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { - // Try to decrypt message body using one of the known sessions for that - // device - /*bool sessionsPassed = false; - // new e2ee TODO: - for (auto &senderSession : sessions) { - if (senderSession == sessions.last()) { - sessionsPassed = true; - } - - const auto decryptedResult = senderSession->decrypt(message); - if (std::holds_alternative(decryptedResult)) { - qCDebug(E2EE) - << "Success decrypting Olm event using existing session" - << senderSession->sessionId(); - return std::get(decryptedResult); - } else { - const auto error = std::get(decryptedResult); - if (message.type() == QOlmMessage::PreKey) { - const auto matches = senderSession->matchesInboundSessionFrom(senderKey, message); - if (auto hasMatch = std::get_if(&matches)) { - if (hasMatch) { - // We had a matching session for a pre-key message, but - // it didn't work. This means something is wrong, so we - // fail now. - qCDebug(E2EE) - << "Error decrypting pre-key message with existing " - "Olm session" - << senderSession->sessionId() << "reason:" << error; - return QString(); - } - } + Q_ASSERT(message.type() == QOlmMessage::PreKey); + for(auto& session : sessions) { + const auto matches = session.second->matchesInboundSessionFrom(senderKey, message); + if(std::holds_alternative(matches) && std::get(matches)) { + qCDebug(E2EE) << "Found inbound session"; + const auto result = session.second->decrypt(message); + if(std::holds_alternative(result)) { + return std::get(result); + } else { + qCDebug(E2EE) << "Failed to decrypt prekey message"; + return {}; } - // Simply keep trying otherwise } } - if (sessionsPassed || sessions.empty()) { - if (message.type() != QOlmMessage::PreKey) { - // Not a pre-key message, we should have had a matching session - if (!sessions.empty()) { - qCDebug(E2EE) << "Error decrypting with existing sessions"; - return QString(); - } - qCDebug(E2EE) << "No existing sessions"; - return QString(); - } - // We have a pre-key message without any matching session, in this - // case we should try to create one. - qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; - QOlmMessage preKeyMessage = QOlmMessage(message.toCiphertext(), QOlmMessage::PreKey); - // new e2ee TODO: - //const auto sessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), preKeyMessage); - - if (const auto error = std::get_if(&sessionResult)) { - qCDebug(E2EE) << "Error decrypting pre-key message when trying " - "to establish a new session:" - << error; - return QString(); - } - - const auto newSession = std::get>(olmAccount->createInboundSessionFrom(senderKey.toUtf8(), preKeyMessage)); - - qCDebug(E2EE) << "Created new Olm session" << newSession->sessionId(); - - const auto decryptedResult = newSession->decrypt(message); - if (const auto error = std::get_if(&decryptedResult)) { - qCDebug(E2EE) - << "Error decrypting pre-key message with new session" - << error; - return QString(); - } - - if (auto error = olmAccount->removeOneTimeKeys(newSession)) { - qCDebug(E2EE) - << "Error removing one time keys" - << error.value(); + qCDebug(E2EE) << "Creating new inbound session"; + auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); + if(std::holds_alternative(newSessionResult)) { + qCWarning(E2EE) << "Failed to create inbound session for" << senderKey; + return {}; + } + std::unique_ptr newSession = std::move(std::get>(newSessionResult)); + // TODO Error handling? + olmAccount->removeOneTimeKeys(newSession); + const auto result = newSession->decrypt(message); + sessions[senderKey] = std::move(newSession); + if(std::holds_alternative(result)) { + return std::get(result); + } else { + qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; + return {}; + } + } + QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) + { + Q_ASSERT(message.type() == QOlmMessage::General); + for(auto& session : sessions) { + const auto result = session.second->decrypt(message); + if(std::holds_alternative(result)) { + return std::get(result); } - //sessions.insert(senderKey, std::move(newSession)); TODO - //return std::get(decryptedResult); - }*/ - return QString(); + } + qCWarning(E2EE) << "Failed to decrypt message"; + return {}; } }; -EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, - float signedKeysProportion, - float oneTimeKeyThreshold, QObject* parent) +EncryptionManager::EncryptionManager(QObject* parent) : QObject(parent) - , d(std::make_unique(std::move(encryptionAccountPickle), - std::move(signedKeysProportion), - std::move(oneTimeKeyThreshold))) + , d(std::make_unique()) { d->q = this; } EncryptionManager::~EncryptionManager() = default; -void EncryptionManager::uploadIdentityKeys(Connection* connection) -{ - // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload - DeviceKeys deviceKeys { - /* - * The ID of the user the device belongs to. Must match the user ID used - * when logging in. The ID of the device these keys belong to. Must - * match the device ID used when logging in. The encryption algorithms - * supported by this device. - */ - connection->userId(), - connection->deviceId(), - SupportedAlgorithms, - /* - * Public identity keys. The names of the properties should be in the - * format :. The keys themselves should be encoded - * as specified by the key algorithm. - */ - { { Curve25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->identityKeys().curve25519 }, - { Ed25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->identityKeys().curve25519 } }, - /* signatures should be provided after the unsigned deviceKeys - generation */ - {} - }; - - QJsonObject deviceKeysJsonObject = toJson(deviceKeys); - /* additionally removing signatures key, - * since we could not initialize deviceKeys - * without an empty signatures value: - */ - deviceKeysJsonObject.remove(QStringLiteral("signatures")); - /* - * Signatures for the device key object. - * A map from user ID, to a map from : to the - * signature. The signature is calculated using the process called Signing - * JSON. - */ - deviceKeys.signatures = { - { connection->userId(), - { { Ed25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->sign(deviceKeysJsonObject) } } } - }; - - d->uploadIdentityKeysJob = connection->callApi(deviceKeys); - connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); - }); -} - -void EncryptionManager::uploadOneTimeKeys(Connection* connection, - bool forceUpdate) -{ - if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { - d->uploadOneTimeKeysInitJob = connection->callApi(); - connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadOneTimeKeysInitJob->oneTimeKeyCounts()); - }); - } - - int signedKeysToUploadCount = - d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0); - int unsignedKeysToUploadCount = - d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0); - - d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount - + unsignedKeysToUploadCount); - - QHash oneTimeKeys = {}; - const auto& olmAccountCurve25519OneTimeKeys = d->olmAccount->oneTimeKeys().curve25519(); - - int oneTimeKeysCounter = 0; - for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); - it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { - QString keyId = it.key(); - QString keyType; - QVariant key; - if (oneTimeKeysCounter < signedKeysToUploadCount) { - QJsonObject message { { QStringLiteral("key"), - it.value() } }; - - QByteArray signedMessage = d->olmAccount->sign(message); - QJsonObject signatures { - { connection->userId(), - QJsonObject { { Ed25519Key + QStringLiteral(":") - + connection->deviceId(), - QString::fromUtf8(signedMessage) } } } - }; - message.insert(QStringLiteral("signatures"), signatures); - key = message; - keyType = SignedCurve25519Key; - } else { - key = it.value(); - keyType = Curve25519Key; - } - ++oneTimeKeysCounter; - oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); - } - d->uploadOneTimeKeysJob = - connection->callApi(none, oneTimeKeys); - connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts()); - }); - // new e2ee TODO: d->olmAccount->markKeysAsPublished(); - qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") - .arg(signedKeysToUploadCount) - .arg(unsignedKeysToUploadCount); -} - -void EncryptionManager::updateOneTimeKeyCounts( - Connection* connection, const QHash& deviceOneTimeKeysCount) -{ - d->oneTimeKeyCounts = deviceOneTimeKeysCount; - if (d->oneTimeKeyShouldUpload()) { - qCDebug(E2EE) << "Uploading new one-time keys."; - uploadOneTimeKeys(connection); - } -} - -void Quotient::EncryptionManager::updateDeviceKeys( - Connection* connection, const QHash& deviceKeys) -{ - d->queryKeysJob = connection->callApi(deviceKeys); - connect(d->queryKeysJob, &BaseJob::success, this, - [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); }); -} - QString EncryptionManager::sessionDecryptMessage( - const QJsonObject& personalCipherObject, const QByteArray& senderKey) + const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account) { QString decrypted; int type = personalCipherObject.value(TypeKeyL).toInt(-1); QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); if (type == 0) { QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); - decrypted = d->sessionDecrypt(preKeyMessage, senderKey); + decrypted = d->sessionDecryptPrekey(preKeyMessage, senderKey, account); } else if (type == 1) { - QOlmMessage message(body, QOlmMessage::PreKey); - decrypted = d->sessionDecrypt(message, senderKey); + QOlmMessage message(body, QOlmMessage::General); + decrypted = d->sessionDecryptGeneral(message, senderKey); } return decrypted; } - -QByteArray EncryptionManager::olmAccountPickle() -{ - // new e2ee TODO: return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? - return {}; -} - -QOlmAccount *EncryptionManager::account() const -{ - return d->olmAccount.get(); -} - -void EncryptionManager::Private::updateKeysToUpload() -{ - for (auto it = targetOneTimeKeyCounts.cbegin(); - it != targetOneTimeKeyCounts.cend(); ++it) { - int numKeys = oneTimeKeyCounts.value(it.key(), 0); - int numToCreate = qMax(it.value() - numKeys, 0); - oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); - } -} - -bool EncryptionManager::Private::oneTimeKeyShouldUpload() -{ - if (oneTimeKeyCounts.empty()) - return true; - for (auto it = targetOneTimeKeyCounts.cbegin(); - it != targetOneTimeKeyCounts.cend(); ++it) { - if (oneTimeKeyCounts.value(it.key(), 0) - < it.value() * oneTimeKeyThreshold) - return true; - } - return false; -} #endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 9d2c8138..17f4f853 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -17,26 +17,10 @@ class EncryptionManager : public QObject { Q_OBJECT public: - // TODO: store constats separately? - // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? - explicit EncryptionManager( - const QByteArray& encryptionAccountPickle = QByteArray(), - float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), - QObject* parent = nullptr); + explicit EncryptionManager(QObject* parent = nullptr); ~EncryptionManager(); - - void uploadIdentityKeys(Connection* connection); - void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); - void - updateOneTimeKeyCounts(Connection* connection, - const QHash& deviceOneTimeKeysCount); - void updateDeviceKeys(Connection* connection, - const QHash& deviceKeys); QString sessionDecryptMessage(const QJsonObject& personalCipherObject, - const QByteArray& senderKey); - QByteArray olmAccountPickle(); - - QOlmAccount* account() const; + const QByteArray& senderKey, std::unique_ptr& account); private: class Private; -- cgit v1.2.3 From 3e51359d1b7891b9a6e4611662c753653c3618bf Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 2 Jun 2021 19:39:34 +0200 Subject: Things --- lib/connection.cpp | 1 + lib/encryptionmanager.h | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2d040e8a..494db170 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -796,6 +796,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) visit(*sessionDecryptMessage(event), [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + qWarning() << "IT'S A ROOMKEY EVENT, RUUUUUUUUUUUUUUUUUN"; detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); } else { qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 17f4f853..db9bff07 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -19,6 +19,7 @@ class EncryptionManager : public QObject { public: explicit EncryptionManager(QObject* parent = nullptr); ~EncryptionManager(); + QString sessionDecryptMessage(const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account); -- cgit v1.2.3 From 1ce9d98cc3957af5a81f6672f3fb86a4dd170ed9 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 24 May 2021 00:40:30 +0200 Subject: Uncomment some stuff --- lib/connection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 494db170..d652c113 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -793,13 +793,13 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) return; } - visit(*sessionDecryptMessage(event), - [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { + visit(*sessionDecryptMessage(ee), + [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - qWarning() << "IT'S A ROOMKEY EVENT, RUUUUUUUUUUUUUUUUUN"; detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); } else { - qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() + qCDebug(E2EE) + << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); } }, -- cgit v1.2.3 From 2c5c990f4248a3112d26c3a92e011655064f3fcf Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 24 May 2021 18:04:30 +0200 Subject: Update the OlmAccountPickle in the accountsettings when the olmaccount changes --- lib/connection.cpp | 8 ++++++-- lib/crypto/qolmaccount.cpp | 6 ++++-- lib/crypto/qolmaccount.h | 9 +++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d652c113..d7115885 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -456,7 +456,11 @@ void Connection::Private::completeSetup(const QString& mxId) AccountSettings accountSettings(data->userId()); // init olmAccount - olmAccount = std::make_unique(data->userId(), data->deviceId()); + olmAccount = std::make_unique(data->userId(), data->deviceId(), q); + connect(olmAccount.get(), &QOlmAccount::needsSave, q, [=](){ + auto pickle = olmAccount->pickle(Unencrypted{}); + AccountSettings(data->userId()).setEncryptionAccountPickle(std::get(pickle)); + }); if (accountSettings.encryptionAccountPickle().isEmpty()) { // create new account and save unpickle data @@ -1283,7 +1287,7 @@ bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } #ifdef Quotient_E2EE_ENABLED QOlmAccount *Connection::olmAccount() const { - return d->olmAccount.get(); //d->encryptionManager->account(); + return d->olmAccount.get(); } #endif // Quotient_E2EE_ENABLED diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 22b1faef..44959ac2 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -46,8 +46,9 @@ QByteArray getRandom(size_t bufferSize) return buffer; } -QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId) - : m_userId(userId) +QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent) + : QObject(parent) + , m_userId(userId) , m_deviceId(deviceId) { } @@ -158,6 +159,7 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const if (error == olm_error()) { throw lastError(m_account); } + Q_EMIT needsSave(); return error; } diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index 54d8506c..1f94ab2b 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -23,10 +23,11 @@ class Connection; //! \code{.cpp} //! const auto olmAccount = new QOlmAccount(this); //! \endcode -class QOlmAccount +class QOlmAccount : public QObject { + Q_OBJECT public: - QOlmAccount(const QString &userId, const QString &deviceId); + QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); ~QOlmAccount(); //! Creates a new instance of OlmAccount. During the instantiation @@ -98,6 +99,10 @@ public: // HACK do not use directly QOlmAccount(OlmAccount *account); OlmAccount *data(); + +Q_SIGNALS: + void needsSave() const; + private: OlmAccount *m_account = nullptr; // owning QString m_userId; -- cgit v1.2.3 From f89c05408bc9a4bf59366f92c50f055d527b9a28 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 12 Jun 2021 18:58:54 +0200 Subject: Remove unrelated changes --- lib/connection.cpp | 7 +++---- lib/encryptionmanager.h | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d7115885..ab5e030b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -797,13 +797,12 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) return; } - visit(*sessionDecryptMessage(ee), - [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { + visit(*sessionDecryptMessage(event), + [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); } else { - qCDebug(E2EE) - << "Encrypted event room id" << roomKeyEvent.roomId() + qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); } }, diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index db9bff07..17f4f853 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -19,7 +19,6 @@ class EncryptionManager : public QObject { public: explicit EncryptionManager(QObject* parent = nullptr); ~EncryptionManager(); - QString sessionDecryptMessage(const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account); -- cgit v1.2.3 From 46e7f0d69db376cf45b354fef69ecba1e4636805 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 12 Jun 2021 19:57:20 +0200 Subject: Emit needsSave when required --- lib/connection.cpp | 3 +-- lib/crypto/qolmaccount.cpp | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index ab5e030b..aaa17cdd 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -460,13 +460,12 @@ void Connection::Private::completeSetup(const QString& mxId) connect(olmAccount.get(), &QOlmAccount::needsSave, q, [=](){ auto pickle = olmAccount->pickle(Unencrypted{}); AccountSettings(data->userId()).setEncryptionAccountPickle(std::get(pickle)); + //TODO handle errors }); if (accountSettings.encryptionAccountPickle().isEmpty()) { // create new account and save unpickle data olmAccount->createNewAccount(); - accountSettings.setEncryptionAccountPickle(std::get(olmAccount->pickle(Unencrypted{}))); - // TODO handle pickle errors auto job = q->callApi(olmAccount->deviceKeys()); connect(job, &BaseJob::failure, q, [=]{ qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 44959ac2..3c1f4bd3 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -68,6 +68,7 @@ void QOlmAccount::createNewAccount() if (error == olm_error()) { throw lastError(m_account); } + Q_EMIT needsSave(); } void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) @@ -218,6 +219,7 @@ std::optional QOlmAccount::removeOneTimeKeys(const std::unique_ptr, QOlmError> QOlmAccount::createOutboun void QOlmAccount::markKeysAsPublished() { olm_account_mark_keys_as_published(m_account); + Q_EMIT needsSave(); } bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, -- cgit v1.2.3 From cdd21a33eabebbad33a610cbe189116caba3ac89 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 00:30:42 +0200 Subject: Fix FTBFS --- lib/connection.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connection.h b/lib/connection.h index 6729b23d..6b1ecdae 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -648,6 +648,7 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); + void encryptionUpdate(Room *room); Q_SIGNALS: /// \brief Initial server resolution has failed /// -- cgit v1.2.3 From d04543ba00335d87a872b0f0c8ff35c85301e096 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 00:42:56 +0200 Subject: Try fixing the CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b707236..a1c8bf1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: arch: ${{ matrix.platform }} - name: Install OpenSSL - if: contains(matrix.os, 'ubuntu') and matrix.e2ee + if: ${{ contains(matrix.os, 'ubuntu') && matrix.e2ee }} run: | sudo apt-get install libssl-dev echo "openssl version" >>$GITHUB_ENV -- 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/connection.cpp | 2 ++ lib/connection.h | 2 ++ lib/room.cpp | 2 ++ 3 files changed, 6 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index aaa17cdd..98686ed0 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -259,7 +259,9 @@ public: Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent), d(new Private(std::make_unique(server))) { +#ifdef Quotient_E2EE_ENABLED d->encryptionManager = new EncryptionManager(this); +#endif d->q = this; // All d initialization should occur before this line } diff --git a/lib/connection.h b/lib/connection.h index 6b1ecdae..c351f93e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -648,7 +648,9 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); +#ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); +#endif Q_SIGNALS: /// \brief Initial server resolution has failed /// 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 9f08e5d865a5500d2926f10de85da2d5dcd063f7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 8 Sep 2021 16:07:45 +0200 Subject: Add a function for decrypting E2EE files/images --- lib/encryptionmanager.cpp | 24 ++++++++++++++++++++++++ lib/encryptionmanager.h | 2 ++ 2 files changed, 26 insertions(+) diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index b9bd6646..48e6701c 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -7,11 +7,13 @@ #include "connection.h" #include "crypto/e2ee.h" +#include "events/encryptedfile.h" #include "csapi/keys.h" #include #include +#include #include "crypto/qolmaccount.h" #include "crypto/qolmsession.h" @@ -21,6 +23,8 @@ #include #include +#include + using namespace Quotient; using std::move; @@ -119,4 +123,24 @@ QString EncryptionManager::sessionDecryptMessage( } return decrypted; } + +QByteArray EncryptionManager::decryptFile(const QByteArray &ciphertext, EncryptedFile* file) +{ + const auto key = QByteArray::fromBase64(file->key.k.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); + const auto iv = QByteArray::fromBase64(file->iv.toLatin1()); + const auto sha256 = QByteArray::fromBase64(file->hashes["sha256"].toLatin1()); + if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return QByteArray(); + } + QByteArray plaintext(ciphertext.size(), 0); + EVP_CIPHER_CTX *ctx; + int length; + ctx = EVP_CIPHER_CTX_new(); + EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)key.data(), (const unsigned char *)iv.data()); + EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); + EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext; +} #endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 17f4f853..96569980 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -12,6 +12,7 @@ namespace Quotient { class Connection; class QOlmAccount; +struct EncryptedFile; class EncryptionManager : public QObject { Q_OBJECT @@ -21,6 +22,7 @@ public: ~EncryptionManager(); QString sessionDecryptMessage(const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account); + static QByteArray decryptFile(const QByteArray &ciphertext, EncryptedFile* encryptedFile); private: class Private; -- cgit v1.2.3 From 1bc8c3c2a177668af00889ae18f26d81dfef4af1 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 24 Oct 2021 16:43:59 +0200 Subject: Remove outdated comment --- autotests/testolmaccount.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index c7edd7ad..4fd129b5 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -552,7 +552,7 @@ void TestOlmAccount::enableEncryption() auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"}); connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) { - room->activateEncryption(); // TODO we should also wait for it + room->activateEncryption(); QSignalSpy spy(room, &Room::encryption); joinedRoom = room->id(); -- cgit v1.2.3 From 0bafc33d70ddfdc8c4015a7f330623c726fe7ef7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 24 Oct 2021 16:47:27 +0200 Subject: Only build the encryptionmanager when encryption is enabled --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 92a9b213..8d5f08af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,7 +128,6 @@ list(APPEND lib_SRCS lib/networksettings.cpp lib/converters.cpp lib/util.cpp - lib/encryptionmanager.cpp lib/eventitem.cpp lib/accountregistry.cpp lib/mxcreply.cpp @@ -171,6 +170,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) lib/crypto/qolmerrors.cpp lib/crypto/qolmsession.cpp lib/crypto/qolmmessage.cpp + lib/encryptionmanager.cpp ) endif() -- cgit v1.2.3 From 6190e2b23a5ef62530a90fc03e264605e716ad2e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 22 Nov 2021 21:44:22 +0100 Subject: Fix olm error parsing --- lib/crypto/qolmerrors.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/crypto/qolmerrors.cpp b/lib/crypto/qolmerrors.cpp index 2c3926de..6db1803c 100644 --- a/lib/crypto/qolmerrors.cpp +++ b/lib/crypto/qolmerrors.cpp @@ -6,15 +6,15 @@ #include "qolmerrors.h" Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { - if (error_raw.compare("BAD_ACCOUNT_KEY")) { + if (!error_raw.compare("BAD_ACCOUNT_KEY")) { return QOlmError::BadAccountKey; - } else if (error_raw.compare("BAD_MESSAGE_KEY_ID")) { + } else if (!error_raw.compare("BAD_MESSAGE_KEY_ID")) { return QOlmError::BadMessageKeyId; - } else if (error_raw.compare("INVALID_BASE64")) { + } else if (!error_raw.compare("INVALID_BASE64")) { return QOlmError::InvalidBase64; - } else if (error_raw.compare("NOT_ENOUGH_RANDOM")) { + } else if (!error_raw.compare("NOT_ENOUGH_RANDOM")) { return QOlmError::NotEnoughRandom; - } else if (error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + } else if (!error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { return QOlmError::OutputBufferTooSmall; } else { return QOlmError::Unknown; -- cgit v1.2.3 From eaf23c0f6bb97fdf16631296ab7016447a99c4f2 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 29 Jan 2021 20:23:42 +0100 Subject: Don't die on broken olm account --- lib/crypto/qolmaccount.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 3c1f4bd3..1de8a0dc 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -77,7 +77,10 @@ void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) const QByteArray key = toKey(mode); const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); if (error == olm_error()) { - throw lastError(m_account); + qCWarning(E2EE) << "Failed to unpickle olm account"; + //TODO: Do something that is not dying + // Probably log the user out since we have no way of getting to the keys + //throw lastError(m_account); } } -- 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/connection.cpp | 5 +- lib/encryptionmanager.cpp | 72 ++++++++++++++++- lib/room.cpp | 195 +++++++++++++++++++++++++++++++++------------- 3 files changed, 215 insertions(+), 57 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 98686ed0..4a220e0d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -464,6 +464,7 @@ void Connection::Private::completeSetup(const QString& mxId) AccountSettings(data->userId()).setEncryptionAccountPickle(std::get(pickle)); //TODO handle errors }); + encryptionManager = new EncryptionManager(q); if (accountSettings.encryptionAccountPickle().isEmpty()) { // create new account and save unpickle data @@ -1891,9 +1892,9 @@ void Connection::Private::saveDevicesList() QFile outFile { q->stateCacheDir().filePath("deviceslist.json") }; if (!outFile.open(QFile::WriteOnly)) { - qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" + qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); - qCWarning(MAIN) << "Caching the rooms state disabled"; + qCWarning(E2EE) << "Caching the rooms state disabled"; cacheState = false; return; } diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 48e6701c..d36d5a7a 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -31,7 +31,6 @@ using std::move; class EncryptionManager::Private { public: explicit Private() - : q(nullptr) { } ~Private() = default; @@ -51,6 +50,73 @@ public: } } } + void loadSessions() { + QFile file { static_cast(q->parent())->stateCacheDir().filePath("olmsessions.json") }; + if(!file.exists() || !file.open(QIODevice::ReadOnly)) { + qCDebug(E2EE) << "No 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) << "Sessions cache is empty"; + return; + } + for(const auto &senderKey : json["sessions"].toObject().keys()) { + auto pickle = json["sessions"].toObject()[senderKey].toString(); + auto sessionResult = QOlmSession::unpickle(pickle.toLatin1(), Unencrypted{}); + if(std::holds_alternative(sessionResult)) { + qCWarning(E2EE) << "Failed to unpickle olm session"; + continue; + } + sessions[senderKey] = std::move(std::get>(sessionResult)); + } + } + void saveSessions() { + QFile outFile { static_cast(q->parent())->stateCacheDir().filePath("olmsessions.json") }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); + qCWarning(E2EE) << "Failed to write olm sessions"; + return; + } + + QJsonObject rootObj { + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), 1 }, + { QStringLiteral("minor"), 0 } } } + }; + { + QJsonObject sessionsJson; + for (const auto &session : sessions) { + auto pickleResult = session.second->pickle(Unencrypted{}); + if(std::holds_alternative(pickleResult)) { + qCWarning(E2EE) << "Failed to pickle session"; + continue; + } + sessionsJson[session.first] = QString(std::get(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) << "Sessions saved to" << outFile.fileName(); + } QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { Q_ASSERT(message.type() == QOlmMessage::PreKey); @@ -60,6 +126,7 @@ public: qCDebug(E2EE) << "Found inbound session"; const auto result = session.second->decrypt(message); if(std::holds_alternative(result)) { + saveSessions(); return std::get(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; @@ -79,6 +146,7 @@ public: const auto result = newSession->decrypt(message); sessions[senderKey] = std::move(newSession); if(std::holds_alternative(result)) { + saveSessions(); return std::get(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; @@ -91,6 +159,7 @@ public: for(auto& session : sessions) { const auto result = session.second->decrypt(message); if(std::holds_alternative(result)) { + saveSessions(); return std::get(result); } } @@ -104,6 +173,7 @@ EncryptionManager::EncryptionManager(QObject* parent) , d(std::make_unique()) { d->q = this; + d->loadSessions(); } EncryptionManager::~EncryptionManager() = default; 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 60bd11426e941f9d349962b8b2ea4bddd9488965 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 17 Aug 2021 18:10:58 +0200 Subject: Don't crash when ToDeviceEvent decryption fails --- lib/connection.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4a220e0d..60ffed09 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -798,8 +798,13 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); return; } + const auto decryptedEvent = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } - visit(*sessionDecryptMessage(event), + visit(*decryptedEvent, [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); -- 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/connection.cpp | 2 +- lib/encryptionmanager.cpp | 4 ++-- lib/room.cpp | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 60ffed09..7a96bc50 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -634,7 +634,7 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) #ifdef Quotient_E2EE_ENABLED if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) { d->isUploadingKeys = true; - d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() - data.deviceOneTimeKeysCount()["signed_curve25519"]); + d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() / 2 - data.deviceOneTimeKeysCount()["signed_curve25519"]); auto keys = d->olmAccount->oneTimeKeys(); auto job = d->olmAccount->createUploadKeyRequest(keys); run(job, ForegroundRequest); diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index d36d5a7a..e8cc7b3a 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -125,8 +125,8 @@ public: if(std::holds_alternative(matches) && std::get(matches)) { qCDebug(E2EE) << "Found inbound session"; const auto result = session.second->decrypt(message); + saveSessions(); if(std::holds_alternative(result)) { - saveSessions(); return std::get(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; @@ -145,8 +145,8 @@ public: olmAccount->removeOneTimeKeys(newSession); const auto result = newSession->decrypt(message); sessions[senderKey] = std::move(newSession); + saveSessions(); if(std::holds_alternative(result)) { - saveSessions(); return std::get(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; 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 --- CMakeLists.txt | 4 +++- lib/connection.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++-- lib/connection.h | 5 +++++ lib/encryptionmanager.cpp | 4 ++-- lib/events/eventcontent.cpp | 1 + lib/room.cpp | 5 ++--- 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d5f08af..3977a9d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,8 @@ if (${PROJECT_NAME}_ENABLE_E2EE) endif() endif() +find_package(Qt${QT_MAJOR_VERSION}Keychain REQUIRED) + # Set up source files list(APPEND lib_SRCS lib/quotient_common.h @@ -325,7 +327,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) find_dependency(OpenSSL)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES}) if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) diff --git a/lib/connection.cpp b/lib/connection.cpp index 7a96bc50..77ab3b72 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -39,6 +39,7 @@ #ifdef Quotient_E2EE_ENABLED # include "crypto/qolmaccount.h" +# include "crypto/qolmutils.h" #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -55,6 +56,13 @@ #include #include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +# include +#else +# include +#endif + using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() @@ -108,6 +116,7 @@ public: QHash> deviceKeys; QueryKeysJob *currentQueryKeysJob = nullptr; bool encryptionUpdateRequired = false; + PicklingMode picklingMode = Unencrypted {}; #endif GetCapabilitiesJob* capabilitiesJob = nullptr; @@ -457,10 +466,40 @@ void Connection::Private::completeSetup(const QString& mxId) #else // Quotient_E2EE_ENABLED AccountSettings accountSettings(data->userId()); + QKeychain::ReadPasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + QEventLoop loop; + QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error() == QKeychain::Error::EntryNotFound) { + picklingMode = Encrypted { getRandom(128) }; + QKeychain::WritePasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + job.setBinaryData(std::get(picklingMode).key); + QEventLoop loop; + QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error()) { + qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString(); + } + } else if(job.error() != QKeychain::Error::NoError) { + //TODO Error, do something + qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error(); + } else { + qCDebug(E2EE) << "Successfully loaded pickling key from keychain"; + picklingMode = Encrypted { job.binaryData() }; + } + // init olmAccount olmAccount = std::make_unique(data->userId(), data->deviceId(), q); connect(olmAccount.get(), &QOlmAccount::needsSave, q, [=](){ - auto pickle = olmAccount->pickle(Unencrypted{}); + auto pickle = olmAccount->pickle(picklingMode); AccountSettings(data->userId()).setEncryptionAccountPickle(std::get(pickle)); //TODO handle errors }); @@ -476,7 +515,7 @@ void Connection::Private::completeSetup(const QString& mxId) } else { // account already existing auto pickle = accountSettings.encryptionAccountPickle(); - olmAccount->unpickle(pickle, Unencrypted{}); + olmAccount->unpickle(pickle, picklingMode); } #endif // Quotient_E2EE_ENABLED emit q->stateChanged(); @@ -1982,4 +2021,9 @@ void Connection::Private::loadDevicesList() } }); } + +PicklingMode Connection::picklingMode() const +{ + return d->picklingMode; +} #endif diff --git a/lib/connection.h b/lib/connection.h index c351f93e..e5cec34b 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -21,6 +21,10 @@ #include +#ifdef Quotient_E2EE_ENABLED +#include "crypto/e2ee.h" +#endif + Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) namespace Quotient { @@ -650,6 +654,7 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); + PicklingMode picklingMode() const; #endif Q_SIGNALS: /// \brief Initial server resolution has failed diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index e8cc7b3a..5c1750c9 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -71,7 +71,7 @@ public: } for(const auto &senderKey : json["sessions"].toObject().keys()) { auto pickle = json["sessions"].toObject()[senderKey].toString(); - auto sessionResult = QOlmSession::unpickle(pickle.toLatin1(), Unencrypted{}); + auto sessionResult = QOlmSession::unpickle(pickle.toLatin1(), static_cast(q->parent())->picklingMode()); if(std::holds_alternative(sessionResult)) { qCWarning(E2EE) << "Failed to unpickle olm session"; continue; @@ -97,7 +97,7 @@ public: { QJsonObject sessionsJson; for (const auto &session : sessions) { - auto pickleResult = session.second->pickle(Unencrypted{}); + auto pickleResult = session.second->pickle(static_cast(q->parent())->picklingMode()); if(std::holds_alternative(pickleResult)) { qCWarning(E2EE) << "Failed to pickle session"; continue; diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 22878d4c..d4cb43ff 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -75,6 +75,7 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("size"), payloadSize); if (mimeType.isValid()) infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + //TODO add encryptedfile } ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) 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/connection.cpp | 15 +++++++ lib/connection.h | 5 +++ lib/jobs/downloadfilejob.cpp | 97 ++++++++++++++++++++++++++++++++++++++------ lib/jobs/downloadfilejob.h | 3 ++ lib/room.cpp | 59 +++++++++++++++++++++++++++ lib/room.h | 3 ++ 6 files changed, 170 insertions(+), 12 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 77ab3b72..4a1130ae 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1017,6 +1017,21 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, return job; } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob* Connection::downloadFile(const QUrl& url, + const QString& key, + const QString& iv, + const QString& sha256, + const QString& localFilename) +{ + auto mediaId = url.authority() + url.path(); + auto idParts = splitMediaId(mediaId); + auto* job = + callApi(idParts.front(), idParts.back(), key, iv, sha256, localFilename); + return job; +} +#endif + CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, diff --git a/lib/connection.h b/lib/connection.h index e5cec34b..f9143d3e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -567,6 +567,11 @@ public Q_SLOTS: DownloadFileJob* downloadFile(const QUrl& url, const QString& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob* downloadFile(const QUrl& url, const QString &key, + const QString& iv, const QString& sha256, + const QString& localFilename = {}); +#endif /** * \brief Create a room (generic method) * This method allows to customize room entirely to your liking, diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 0b0531ad..7a7a46f6 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -7,8 +7,25 @@ #include #include -using namespace Quotient; +#ifdef Quotient_E2EE_ENABLED +# include +# include + +QByteArray decrypt(const QByteArray &ciphertext, const QByteArray &key, const QByteArray &iv) +{ + QByteArray plaintext(ciphertext.size(), 0); + EVP_CIPHER_CTX *ctx; + int length; + ctx = EVP_CIPHER_CTX_new(); + EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)key.data(), (const unsigned char *)iv.data()); + EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); + EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext; +} +#endif +using namespace Quotient; class DownloadFileJob::Private { public: Private() : tempFile(new QTemporaryFile()) {} @@ -20,6 +37,12 @@ public: QScopedPointer targetFile; QScopedPointer tempFile; + +#ifdef Quotient_E2EE_ENABLED + QByteArray key; + QByteArray iv; + QByteArray sha256; +#endif }; QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) @@ -37,6 +60,23 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, setObjectName(QStringLiteral("DownloadFileJob")); } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob::DownloadFileJob(const QString& serverName, + const QString& mediaId, + const QString& key, + const QString& iv, + const QString& sha256, + const QString& localFilename) + : GetContentJob(serverName, mediaId) + , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) +{ + setObjectName(QStringLiteral("DownloadFileJob")); + auto _key = key; + d->key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); + d->iv = QByteArray::fromBase64(iv.toLatin1()); + d->sha256 = QByteArray::fromBase64(sha256.toLatin1()); +} +#endif QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); @@ -51,7 +91,7 @@ void DownloadFileJob::doPrepare() setStatus(FileError, "Could not open the target file for writing"); return; } - if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) { + if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); @@ -99,18 +139,51 @@ void DownloadFileJob::beforeAbandon() BaseJob::Status DownloadFileJob::prepareResult() { if (d->targetFile) { - d->targetFile->close(); - if (!d->targetFile->remove()) { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; - return { FileError, "Couldn't finalise the download" }; +#ifdef Quotient_E2EE_ENABLED + if(d->key.size() != 0) { + d->tempFile->seek(0); + QByteArray encrypted = d->tempFile->readAll(); + if(d->sha256 != QCryptographicHash::hash(encrypted, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return IncorrectResponse; + } + auto decrypted = decrypt(encrypted, d->key, d->iv); + d->targetFile->write(decrypted); + d->targetFile->remove(); + } else { +#endif + d->targetFile->close(); + if (!d->targetFile->remove()) { + qCWarning(JOBS) << "Failed to remove the target file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!d->tempFile->rename(d->targetFile->fileName())) { + qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + << "to" << d->targetFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } +#ifdef Quotient_E2EE_ENABLED } - if (!d->tempFile->rename(d->targetFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() - << "to" << d->targetFile->fileName(); - return { FileError, "Couldn't finalise the download" }; +#endif + } else { +#ifdef Quotient_E2EE_ENABLED + if(d->key.size() != 0) { + d->tempFile->seek(0); + auto encrypted = d->tempFile->readAll(); + + if(d->sha256 != QCryptographicHash::hash(encrypted, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return IncorrectResponse; + } + auto decrypted = decrypt(encrypted, d->key, d->iv); + d->tempFile->write(decrypted); + } else { +#endif + d->tempFile->close(); +#ifdef Quotient_E2EE_ENABLED } - } else - d->tempFile->close(); +#endif + } qCDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index 0752af89..f000b991 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -14,6 +14,9 @@ public: DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& key, const QString& iv, const QString& sha256, const QString& localFilename = {}); +#endif QString targetFileName() const; private: 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); diff --git a/lib/room.h b/lib/room.h index 85c51a87..ac21d08c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -852,6 +852,9 @@ public Q_SLOTS: const QString& overrideContentType = {}); // If localFilename is empty a temporary file is created void downloadFile(const QString& eventId, const QUrl& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + void downloadFile(const QString& eventId, const QString& key, const QString &iv, const QString &sha256, const QUrl& localFilename = {}); +#endif void cancelFileTransfer(const QString& id); //! \brief Set a given event as last read and post a read receipt on it -- 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/jobs/downloadfilejob.cpp | 2 +- lib/room.cpp | 67 ++++++-------------------------------------- lib/room.h | 3 -- 3 files changed, 10 insertions(+), 62 deletions(-) diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 7a7a46f6..e82271eb 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -149,7 +149,7 @@ BaseJob::Status DownloadFileJob::prepareResult() } auto decrypted = decrypt(encrypted, d->key, d->iv); d->targetFile->write(decrypted); - d->targetFile->remove(); + d->tempFile->remove(); } else { #endif d->targetFile->close(); 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) { diff --git a/lib/room.h b/lib/room.h index ac21d08c..85c51a87 100644 --- a/lib/room.h +++ b/lib/room.h @@ -852,9 +852,6 @@ public Q_SLOTS: const QString& overrideContentType = {}); // If localFilename is empty a temporary file is created void downloadFile(const QString& eventId, const QUrl& localFilename = {}); -#ifdef Quotient_E2EE_ENABLED - void downloadFile(const QString& eventId, const QString& key, const QString &iv, const QString &sha256, const QUrl& localFilename = {}); -#endif void cancelFileTransfer(const QString& id); //! \brief Set a given event as last read and post a read receipt on it -- 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/connection.cpp | 6 ++---- lib/connection.h | 3 +-- lib/jobs/downloadfilejob.cpp | 48 +++++++++++--------------------------------- lib/jobs/downloadfilejob.h | 3 ++- lib/mxcreply.cpp | 36 +++++++++++++++++++++++++++++++-- lib/networkaccessmanager.cpp | 6 ++++++ lib/networkaccessmanager.h | 2 +- lib/room.cpp | 2 +- 8 files changed, 59 insertions(+), 47 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4a1130ae..d8e98bb0 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1019,15 +1019,13 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, #ifdef Quotient_E2EE_ENABLED DownloadFileJob* Connection::downloadFile(const QUrl& url, - const QString& key, - const QString& iv, - const QString& sha256, + const EncryptedFile file, const QString& localFilename) { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); auto* job = - callApi(idParts.front(), idParts.back(), key, iv, sha256, localFilename); + callApi(idParts.front(), idParts.back(), file, localFilename); return job; } #endif diff --git a/lib/connection.h b/lib/connection.h index f9143d3e..d0945aa4 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -568,8 +568,7 @@ public Q_SLOTS: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob* downloadFile(const QUrl& url, const QString &key, - const QString& iv, const QString& sha256, + DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile file, const QString& localFilename = {}); #endif /** diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index e82271eb..2fba1973 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -9,20 +9,8 @@ #ifdef Quotient_E2EE_ENABLED # include -# include - -QByteArray decrypt(const QByteArray &ciphertext, const QByteArray &key, const QByteArray &iv) -{ - QByteArray plaintext(ciphertext.size(), 0); - EVP_CIPHER_CTX *ctx; - int length; - ctx = EVP_CIPHER_CTX_new(); - EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)key.data(), (const unsigned char *)iv.data()); - EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); - EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext; -} +# include "encryptionmanager.h" +# include "events/encryptedfile.h" #endif using namespace Quotient; @@ -39,9 +27,7 @@ public: QScopedPointer tempFile; #ifdef Quotient_E2EE_ENABLED - QByteArray key; - QByteArray iv; - QByteArray sha256; + Omittable encryptedFile; #endif }; @@ -63,18 +49,13 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, #ifdef Quotient_E2EE_ENABLED DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, - const QString& key, - const QString& iv, - const QString& sha256, + const EncryptedFile file, const QString& localFilename) : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); - auto _key = key; - d->key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); - d->iv = QByteArray::fromBase64(iv.toLatin1()); - d->sha256 = QByteArray::fromBase64(sha256.toLatin1()); + d->encryptedFile = file; } #endif QString DownloadFileJob::targetFileName() const @@ -140,14 +121,12 @@ BaseJob::Status DownloadFileJob::prepareResult() { if (d->targetFile) { #ifdef Quotient_E2EE_ENABLED - if(d->key.size() != 0) { + if (d->encryptedFile.has_value()) { d->tempFile->seek(0); QByteArray encrypted = d->tempFile->readAll(); - if(d->sha256 != QCryptographicHash::hash(encrypted, QCryptographicHash::Sha256)) { - qCWarning(E2EE) << "Hash verification failed for file"; - return IncorrectResponse; - } - auto decrypted = decrypt(encrypted, d->key, d->iv); + + EncryptedFile file = *d->encryptedFile; + auto decrypted = EncryptionManager::decryptFile(encrypted, &file); d->targetFile->write(decrypted); d->tempFile->remove(); } else { @@ -167,15 +146,12 @@ BaseJob::Status DownloadFileJob::prepareResult() #endif } else { #ifdef Quotient_E2EE_ENABLED - if(d->key.size() != 0) { + if (d->encryptedFile.has_value()) { d->tempFile->seek(0); auto encrypted = d->tempFile->readAll(); - if(d->sha256 != QCryptographicHash::hash(encrypted, QCryptographicHash::Sha256)) { - qCWarning(E2EE) << "Hash verification failed for file"; - return IncorrectResponse; - } - auto decrypted = decrypt(encrypted, d->key, d->iv); + EncryptedFile file = *d->encryptedFile; + auto decrypted = EncryptionManager::decryptFile(encrypted, &file); d->tempFile->write(decrypted); } else { #endif diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index f000b991..67a3e95f 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -4,6 +4,7 @@ #pragma once #include "csapi/content-repo.h" +#include "events/encryptedfile.h" namespace Quotient { class DownloadFileJob : public GetContentJob { @@ -15,7 +16,7 @@ public: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& key, const QString& iv, const QString& sha256, const QString& localFilename = {}); + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile file, const QString& localFilename = {}); #endif QString targetFileName() const; diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 0b6643fc..65078301 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -3,8 +3,17 @@ #include "mxcreply.h" +#include +#include +#include "accountregistry.h" +#include "connection.h" #include "room.h" +#ifdef Quotient_E2EE_ENABLED +#include "encryptionmanager.h" +#include "events/encryptedfile.h" +#endif + using namespace Quotient; class MxcReply::Private @@ -14,6 +23,8 @@ public: : m_reply(r) {} QNetworkReply* m_reply; + Omittable m_encryptedFile; + QIODevice* m_device = nullptr; }; MxcReply::MxcReply(QNetworkReply* reply) @@ -31,11 +42,32 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) : d(std::make_unique(reply)) { reply->setParent(this); - connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() { + connect(d->m_reply, &QNetworkReply::finished, this, [this]() { setError(d->m_reply->error(), d->m_reply->errorString()); + +#ifdef Quotient_E2EE_ENABLED + if(!d->m_encryptedFile.has_value()) { + d->m_device = d->m_reply; + } else { + EncryptedFile file = *d->m_encryptedFile; + auto buffer = new QBuffer(this); + buffer->setData(EncryptionManager::decryptFile(d->m_reply->readAll(), &file)); + d->m_device = buffer; + } setOpenMode(ReadOnly); emit finished(); +#else + d->m_device = d->m_reply; +#endif }); + +#ifdef Quotient_E2EE_ENABLED + auto eventIt = room->findInTimeline(eventId); + if(eventIt != room->historyEdge()) { + auto event = eventIt->viewAs(); + d->m_encryptedFile = event->content()->fileInfo()->file; + } +#endif } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -61,7 +93,7 @@ MxcReply::MxcReply() qint64 MxcReply::readData(char *data, qint64 maxSize) { - return d->m_reply->read(data, maxSize); + return d->m_device->read(data, maxSize); } void MxcReply::abort() diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index d0380cec..c660cff8 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -12,6 +12,12 @@ #include #include #include +#include "accountregistry.h" +#include "mxcreply.h" +#include "connection.h" +#include "events/eventcontent.h" + +#include "room.h" using namespace Quotient; diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 7643302f..efa41994 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -9,7 +9,7 @@ namespace Quotient { class Room; -class Connection; + class NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: 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 f42b91eb52ea408c6aedd4954cac7eb02e7b0df4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 Aug 2021 00:14:35 +0200 Subject: Try adding qtkeychain to github CI --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1c8bf1d..6f1db8ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,6 +121,14 @@ jobs: cmake --build olm/build --target install echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV + - name: Build and install QtKeychain + if: matrix.e2ee + run: | + cd ${{ runner.workspace }} + git clone https://github.com/frankosterfeld/qtkeychain.git + cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS + cmake --build qtkeychain/build --target install + - name: Pull CS API and build GTAD if: matrix.update-api run: | -- 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(+) 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/connection.cpp | 54 ++++++++++++++++++++++++---------------------- lib/crypto/qolmsession.cpp | 4 +--- lib/encryptionmanager.cpp | 2 +- lib/mxcreply.cpp | 1 + lib/room.cpp | 4 +++- lib/settings.cpp | 6 ------ 6 files changed, 34 insertions(+), 37 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d8e98bb0..f36166ff 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -830,33 +830,35 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED - qWarning() << "Consuming to device events" << toDeviceEvents.size(); - if(toDeviceEvents.size() > 0) - visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { - if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); - return; - } - const auto decryptedEvent = sessionDecryptMessage(event); - if(!decryptedEvent) { - qCWarning(E2EE) << "Failed to decrypt event" << event.id(); - return; - } + if(toDeviceEvents.size() > 0) { + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; + visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { + if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); + return; + } + qWarning() << event.fullJson(); + const auto decryptedEvent = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } - visit(*decryptedEvent, - [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); - } else { - qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - } - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); - }); - }); + visit(*decryptedEvent, + [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); + } else { + qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { + qCDebug(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); + }); + } #endif } diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp index 2068a7d9..a327a643 100644 --- a/lib/crypto/qolmsession.cpp +++ b/lib/crypto/qolmsession.cpp @@ -46,9 +46,7 @@ std::variant, QOlmError> QOlmSession::createInbound if (error == olm_error()) { const auto lastErr = lastError(olmSession); - if (lastErr == QOlmError::NotEnoughRandom) { - qCCritical(E2EE) << "Error when creating inbound session" << lastErr; - } + qCWarning(E2EE) << "Error when creating inbound session" << lastErr; return lastErr; } diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 5c1750c9..81c13e50 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -137,7 +137,7 @@ public: qCDebug(E2EE) << "Creating new inbound session"; auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); if(std::holds_alternative(newSessionResult)) { - qCWarning(E2EE) << "Failed to create inbound session for" << senderKey; + qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get(newSessionResult); return {}; } std::unique_ptr newSession = std::move(std::get>(newSessionResult)); diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 65078301..639c1324 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -52,6 +52,7 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) EncryptedFile file = *d->m_encryptedFile; auto buffer = new QBuffer(this); buffer->setData(EncryptionManager::decryptFile(d->m_reply->readAll(), &file)); + buffer->open(ReadOnly); d->m_device = buffer; } setOpenMode(ReadOnly); 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)); } } } diff --git a/lib/settings.cpp b/lib/settings.cpp index ed9082b0..f9b4f471 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -136,18 +136,12 @@ void AccountSettings::clearAccessToken() QByteArray AccountSettings::encryptionAccountPickle() { - QString passphrase = ""; // FIXME: add QtKeychain return value("encryption_account_pickle", "").toByteArray(); } void AccountSettings::setEncryptionAccountPickle( const QByteArray& encryptionAccountPickle) { - qCWarning(MAIN) - << "Saving encryption_account_pickle to QSettings is insecure." - " Developers, do it manually or contribute to share QtKeychain " - "logic to libQuotient."; - QString passphrase = ""; // FIXME: add QtKeychain setValue("encryption_account_pickle", encryptionAccountPickle); } -- cgit v1.2.3 From cab1c772abaf380f30a504231fc06b070feb09ec Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 15 Nov 2021 21:35:11 +0100 Subject: Save olm account on shutdown --- lib/connection.cpp | 18 +++++++++++++----- lib/connection.h | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index f36166ff..20b4a113 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -270,6 +270,9 @@ Connection::Connection(const QUrl& server, QObject* parent) { #ifdef Quotient_E2EE_ENABLED d->encryptionManager = new EncryptionManager(this); + connect(qApp, &QCoreApplication::aboutToQuit, this, [this](){ + saveOlmAccount(); + }); #endif d->q = this; // All d initialization should occur before this line } @@ -498,11 +501,8 @@ void Connection::Private::completeSetup(const QString& mxId) // init olmAccount olmAccount = std::make_unique(data->userId(), data->deviceId(), q); - connect(olmAccount.get(), &QOlmAccount::needsSave, q, [=](){ - auto pickle = olmAccount->pickle(picklingMode); - AccountSettings(data->userId()).setEncryptionAccountPickle(std::get(pickle)); - //TODO handle errors - }); + connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); + encryptionManager = new EncryptionManager(q); if (accountSettings.encryptionAccountPickle().isEmpty()) { @@ -2042,3 +2042,11 @@ PicklingMode Connection::picklingMode() const return d->picklingMode; } #endif + +void Connection::saveOlmAccount() +{ + qCDebug(E2EE) << "Saving olm account"; + auto pickle = d->olmAccount->pickle(d->picklingMode); + AccountSettings(d->data->userId()).setEncryptionAccountPickle(std::get(pickle)); + //TODO handle errors +} diff --git a/lib/connection.h b/lib/connection.h index d0945aa4..f5f06471 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -480,6 +480,9 @@ public: setUserFactory(defaultUserFactory()); } + /// Saves the olm account data to disk. Usually doesn't need to be called manually. + void saveOlmAccount(); + public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// -- 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/connection.cpp | 16 ++++++++++++++-- lib/connection.h | 2 ++ lib/encryptionmanager.cpp | 4 ++-- lib/room.cpp | 5 ++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 20b4a113..cd4c9838 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1949,7 +1949,7 @@ void Connection::Private::saveDevicesList() QElapsedTimer et; et.start(); - QFile outFile { q->stateCacheDir().filePath("deviceslist.json") }; + QFile outFile { q->e2eeDataDir() + QStringLiteral("/deviceslist.json") }; if (!outFile.open(QFile::WriteOnly)) { qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); @@ -1997,7 +1997,7 @@ void Connection::Private::saveDevicesList() void Connection::Private::loadDevicesList() { - QFile file { q->stateCacheDir().filePath("deviceslist.json") }; + QFile file { q->e2eeDataDir() + QStringLiteral("/deviceslist.json") }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No devicesList cache exists. Creating new"; return; @@ -2050,3 +2050,15 @@ void Connection::saveOlmAccount() AccountSettings(d->data->userId()).setEncryptionAccountPickle(std::get(pickle)); //TODO handle errors } + +QString Connection::e2eeDataDir() const +{ + auto safeUserId = userId(); + safeUserId.replace(':', '_'); + const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) % '/' + % safeUserId % '/'; + QDir dir; + if (!dir.exists(path)) + dir.mkpath(path); + return path; +} diff --git a/lib/connection.h b/lib/connection.h index f5f06471..d1e3a992 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -399,6 +399,8 @@ public: bool lazyLoading() const; void setLazyLoading(bool newValue); + QString e2eeDataDir() const; + /*! Start a pre-created job object on this connection */ Q_INVOKABLE BaseJob* run(BaseJob* job, RunningPolicy runningPolicy = ForegroundRequest); diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 81c13e50..36cfb34c 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -51,7 +51,7 @@ public: } } void loadSessions() { - QFile file { static_cast(q->parent())->stateCacheDir().filePath("olmsessions.json") }; + QFile file { static_cast(q->parent())->e2eeDataDir() + QStringLiteral("/olmsessions.json") }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No sessions cache exists."; return; @@ -80,7 +80,7 @@ public: } } void saveSessions() { - QFile outFile { static_cast(q->parent())->stateCacheDir().filePath("olmsessions.json") }; + QFile outFile { static_cast(q->parent())->e2eeDataDir() + QStringLiteral("/olmsessions.json") }; if (!outFile.open(QFile::WriteOnly)) { qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); 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/connection.cpp | 12 +++++++++++- lib/connection.h | 1 + lib/room.cpp | 1 - 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index cd4c9838..6ed116c4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -837,7 +837,6 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); return; } - qWarning() << event.fullJson(); const auto decryptedEvent = sessionDecryptMessage(event); if(!decryptedEvent) { qCWarning(E2EE) << "Failed to decrypt event" << event.id(); @@ -2062,3 +2061,14 @@ QString Connection::e2eeDataDir() const dir.mkpath(path); return path; } + +QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) +{ + auto room = provideRoom(notification["room_id"].toString()); + auto event = makeEvent(notification["event"].toObject()); + auto decrypted = room->decryptMessage(*event); + if(!decrypted) { + return QJsonObject(); + } + return decrypted->fullJson(); +} diff --git a/lib/connection.h b/lib/connection.h index d1e3a992..824c7fb4 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -664,6 +664,7 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); PicklingMode picklingMode() const; + QJsonObject decryptNotification(const QJsonObject ¬ification); #endif Q_SIGNALS: /// \brief Initial server resolution has failed 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 545852ca45fadb3ee43072763e81cbfba0366e25 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 19 Nov 2021 21:49:46 +0100 Subject: Fix compilation --- CMakeLists.txt | 2 +- lib/connection.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3977a9d0..dbb43f89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,7 +109,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) endif() endif() -find_package(Qt${QT_MAJOR_VERSION}Keychain REQUIRED) +find_package(${Qt}Keychain REQUIRED) # Set up source files list(APPEND lib_SRCS diff --git a/lib/connection.cpp b/lib/connection.cpp index 6ed116c4..95ed1eb6 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2045,9 +2045,11 @@ PicklingMode Connection::picklingMode() const void Connection::saveOlmAccount() { qCDebug(E2EE) << "Saving olm account"; +#ifdef Quotient_E2EE_ENABLED auto pickle = d->olmAccount->pickle(d->picklingMode); AccountSettings(d->data->userId()).setEncryptionAccountPickle(std::get(pickle)); //TODO handle errors +#endif } QString Connection::e2eeDataDir() const @@ -2062,6 +2064,7 @@ QString Connection::e2eeDataDir() const return path; } +#ifdef Quotient_E2EE_ENABLED QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) { auto room = provideRoom(notification["room_id"].toString()); @@ -2072,3 +2075,4 @@ QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) } return decrypted->fullJson(); } +#endif -- cgit v1.2.3 From 6896d3e1bd57c398bd4e1ee9badac87dc66ccea7 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 20 Nov 2021 00:16:14 +0100 Subject: We always need qtkeychain --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f1db8ef..c2076dea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,6 @@ jobs: echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - name: Build and install QtKeychain - if: matrix.e2ee run: | cd ${{ runner.workspace }} git clone https://github.com/frankosterfeld/qtkeychain.git -- 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/encryptionmanager.cpp | 2 +- lib/room.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 36cfb34c..b09e5260 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -38,7 +38,7 @@ public: EncryptionManager* q; // A map from senderKey to InboundSession - std::map> sessions; // TODO: cache + UnorderedMap> sessions; // TODO: cache void updateDeviceKeys( const QHash>& deviceKeys) 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 e4802dc967c640ebac1d0bc7db55b47b79233544 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 20 Nov 2021 20:54:34 +0100 Subject: Try fixing windows CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2076dea..d0c391be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,7 +123,7 @@ jobs: - name: Build and install QtKeychain run: | - cd ${{ runner.workspace }} + cd .. git clone https://github.com/frankosterfeld/qtkeychain.git cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS cmake --build qtkeychain/build --target install -- 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/connection.cpp | 8 ++++---- lib/encryptionmanager.cpp | 2 +- lib/jobs/downloadfilejob.cpp | 2 +- lib/jobs/downloadfilejob.h | 2 +- lib/mxcreply.cpp | 3 +-- lib/room.cpp | 11 +++++------ 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 95ed1eb6..df9ff445 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -57,7 +57,7 @@ #include -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR >= 6 # include #else # include @@ -830,7 +830,7 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED - if(toDeviceEvents.size() > 0) { + if (!toDeviceEvents.empty()) { qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { @@ -1020,7 +1020,7 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, #ifdef Quotient_E2EE_ENABLED DownloadFileJob* Connection::downloadFile(const QUrl& url, - const EncryptedFile file, + const EncryptedFile& file, const QString& localFilename) { auto mediaId = url.authority() + url.path(); @@ -1996,7 +1996,7 @@ void Connection::Private::saveDevicesList() void Connection::Private::loadDevicesList() { - QFile file { q->e2eeDataDir() + QStringLiteral("/deviceslist.json") }; + QFile file { q->e2eeDataDir() % "/deviceslist.json" }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No devicesList cache exists. Creating new"; return; diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index b09e5260..c816eda7 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -51,7 +51,7 @@ public: } } void loadSessions() { - QFile file { static_cast(q->parent())->e2eeDataDir() + QStringLiteral("/olmsessions.json") }; + QFile file { static_cast(q->parent())->e2eeDataDir() % "/olmsessions.json" }; if(!file.exists() || !file.open(QIODevice::ReadOnly)) { qCDebug(E2EE) << "No sessions cache exists."; return; diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 2fba1973..0b4cf6d2 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -49,7 +49,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, #ifdef Quotient_E2EE_ENABLED DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, - const EncryptedFile file, + const EncryptedFile& file, const QString& localFilename) : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index 67a3e95f..90d80478 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -16,7 +16,7 @@ public: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile file, const QString& localFilename = {}); + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {}); #endif QString targetFileName() const; diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 639c1324..2ad49c2c 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -3,8 +3,7 @@ #include "mxcreply.h" -#include -#include +#include #include "accountregistry.h" #include "connection.h" #include "room.h" 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/connection.h | 2 +- lib/encryptionmanager.cpp | 5 ----- lib/networkaccessmanager.cpp | 6 ------ lib/room.cpp | 21 +++++++-------------- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 824c7fb4..d2347d1d 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -573,7 +573,7 @@ public Q_SLOTS: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile file, + DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file, const QString& localFilename = {}); #endif /** diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index c816eda7..84282dbf 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -107,12 +107,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) << "Sessions saved to" << outFile.fileName(); diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index c660cff8..d0380cec 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -12,12 +12,6 @@ #include #include #include -#include "accountregistry.h" -#include "mxcreply.h" -#include "connection.h" -#include "events/eventcontent.h" - -#include "room.h" using namespace Quotient; 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/events/encryptedevent.cpp | 22 ++++++++++++++++++ lib/events/encryptedevent.h | 1 + lib/room.cpp | 53 ++++++++++++++----------------------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 0290f973..c9257584 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" +#include "roommessageevent.h" using namespace Quotient; @@ -30,3 +31,24 @@ EncryptedEvent::EncryptedEvent(const QJsonObject& obj) { qCDebug(E2EE) << "Encrypted event from" << senderId(); } + +RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const +{ + auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + eventObject["event_id"] = id(); + eventObject["sender"] = senderId(); + eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); + if(contentJson().contains("m.relates_to")) { + auto relates = contentJson()["m.relates_to"].toObject(); + auto content = eventObject["content"].toObject(); + content["m.relates_to"] = relates; + eventObject["content"] = content; + } + if(unsignedJson().contains("redacts")) { + auto redacts = unsignedJson()["redacts"].toString(); + auto unsign = eventObject["unsigned"].toObject(); + unsign["redacts"] = redacts; + eventObject["unsigned"] = unsign; + } + return makeEvent(eventObject); +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 1d7ea913..28398827 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -61,6 +61,7 @@ public: /* device_id and session_id are required with Megolm */ QString deviceId() const { return contentPart(DeviceIdKeyL); } QString sessionId() const { return contentPart(SessionIdKeyL); } + RoomEventPtr createDecrypted(const QString &decrypted) const; }; REGISTER_EVENT_TYPE(EncryptedEvent) 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 --- autotests/testolmsession.cpp | 16 ++++++++-------- lib/connection.cpp | 10 +++++----- lib/crypto/qolmaccount.cpp | 14 +++++++------- lib/crypto/qolmaccount.h | 10 ++++++---- lib/crypto/qolmsession.cpp | 10 +++++----- lib/crypto/qolmsession.h | 3 +-- lib/encryptionmanager.cpp | 12 +++++++----- lib/room.cpp | 4 ++-- 8 files changed, 41 insertions(+), 38 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index dba78277..750b804e 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -7,7 +7,7 @@ using namespace Quotient; -std::pair, std::unique_ptr> createSessionPair() +std::pair createSessionPair() { QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr"); @@ -20,7 +20,7 @@ std::pair, std::unique_ptr> createSess const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk"); const QByteArray identityKeyB("q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg"); const QByteArray oneTimeKeyB("oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ"); - auto outbound = std::get>(accountA + auto outbound = std::get(accountA .createOutboundSession(identityKeyB, oneTimeKeyB)); const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey @@ -29,8 +29,8 @@ std::pair, std::unique_ptr> createSess // We can't call QFail here because it's an helper function returning a value throw "Wrong first message type received, can't create session"; } - auto inbound = std::get>(accountB.createInboundSession(preKey)); - return std::make_pair, std::unique_ptr>(std::move(inbound), std::move(outbound)); + auto inbound = std::get(accountB.createInboundSession(preKey)); + return std::make_pair(std::move(inbound), std::move(outbound)); } void TestOlmSession::olmOutboundSessionCreation() @@ -56,17 +56,17 @@ void TestOlmSession::olmEncryptDecrypt() void TestOlmSession::correctSessionOrdering() { // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM - auto session1 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{})); + auto session1 = std::get(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{})); // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0 - auto session2 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {})); + auto session2 = std::get(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {})); // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME - auto session3 = std::get>(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{})); + auto session3 = std::get(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{})); const auto session1Id = session1->sessionId(); const auto session2Id = session2->sessionId(); const auto session3Id = session3->sessionId(); - std::vector> sessionList; + std::vector sessionList; sessionList.push_back(std::move(session1)); sessionList.push_back(std::move(session2)); sessionList.push_back(std::move(session3)); diff --git a/lib/connection.cpp b/lib/connection.cpp index df9ff445..a7af1477 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -509,7 +509,7 @@ void Connection::Private::completeSetup(const QString& mxId) // create new account and save unpickle data olmAccount->createNewAccount(); auto job = q->callApi(olmAccount->deviceKeys()); - connect(job, &BaseJob::failure, q, [=]{ + connect(job, &BaseJob::failure, q, [job]{ qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); }); } else { @@ -677,10 +677,10 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) auto keys = d->olmAccount->oneTimeKeys(); auto job = d->olmAccount->createUploadKeyRequest(keys); run(job, ForegroundRequest); - connect(job, &BaseJob::success, this, [=](){ + connect(job, &BaseJob::success, this, [this](){ d->olmAccount->markKeysAsPublished(); }); - connect(job, &BaseJob::result, this, [=](){ + connect(job, &BaseJob::result, this, [this](){ d->isUploadingKeys = false; }); } @@ -1903,7 +1903,7 @@ void Connection::Private::loadOutdatedUserDevices() } auto queryKeysJob = q->callApi(users); currentQueryKeysJob = queryKeysJob; - connect(queryKeysJob, &BaseJob::success, q, [=](){ + connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){ currentQueryKeysJob = nullptr; const auto data = queryKeysJob->deviceKeys(); for(const auto &[user, keys] : asKeyValueRange(data)) { @@ -2024,7 +2024,7 @@ void Connection::Private::loadDevicesList() deviceKeys = fromJson>>(json["devices_list"].toObject()); auto oldToken = json["sync_token"].toString(); auto changesJob = q->callApi(oldToken, q->nextBatchToken()); - connect(changesJob, &BaseJob::success, q, [=](){ + connect(changesJob, &BaseJob::success, q, [this, changesJob](){ bool hasNewOutdatedUser = false; for(const auto &user : changesJob->changed()) { outdatedUsers += user; diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp index 1de8a0dc..5c9f5db4 100644 --- a/lib/crypto/qolmaccount.cpp +++ b/lib/crypto/qolmaccount.cpp @@ -179,13 +179,13 @@ OneTimeKeys QOlmAccount::oneTimeKeys() const const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); OneTimeKeys oneTimeKeys; - for (const QJsonValue &key1 : json.keys()) { - auto oneTimeKeyObject = json[key1.toString()].toObject(); + for (const QString& key1 : json.keys()) { + auto oneTimeKeyObject = json[key1].toObject(); auto keyMap = QMap(); for (const QString &key2 : oneTimeKeyObject.keys()) { keyMap[key2] = oneTimeKeyObject[key2].toString(); } - oneTimeKeys.keys[key1.toString()] = keyMap; + oneTimeKeys.keys[key1] = keyMap; } return oneTimeKeys; } @@ -215,7 +215,7 @@ QByteArray QOlmAccount::signOneTimeKey(const QString &key) const return sign(j.toJson(QJsonDocument::Compact)); } -std::optional QOlmAccount::removeOneTimeKeys(const std::unique_ptr &session) const +std::optional QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const { const auto error = olm_remove_one_time_keys(m_account, session->raw()); @@ -266,19 +266,19 @@ UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKey return new UploadKeysJob(keys, oneTimeKeysSigned); } -std::variant, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) +std::variant QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSession(this, preKeyMessage); } -std::variant, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) +std::variant QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); } -std::variant, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +std::variant QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) { return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); } diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h index 1f94ab2b..dd461e8b 100644 --- a/lib/crypto/qolmaccount.h +++ b/lib/crypto/qolmaccount.h @@ -19,6 +19,8 @@ namespace Quotient { class QOlmSession; class Connection; +using QOlmSessionPtr = std::unique_ptr; + //! An olm account manages all cryptographic keys used on a device. //! \code{.cpp} //! const auto olmAccount = new QOlmAccount(this); @@ -77,22 +79,22 @@ public: DeviceKeys deviceKeys() const; //! Remove the one time key used to create the supplied session. - [[nodiscard]] std::optional removeOneTimeKeys(const std::unique_ptr &session) const; + [[nodiscard]] std::optional removeOneTimeKeys(const QOlmSessionPtr &session) const; //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param message An Olm pre-key message that was encrypted for this account. - std::variant, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage); + std::variant createInboundSession(const QOlmMessage &preKeyMessage); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param theirIdentityKey - The identity key of the Olm account that //! encrypted this Olm message. - std::variant, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); + std::variant createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); //! Creates an outbound session for sending messages to a specific /// identity and one time key. - std::variant, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + std::variant createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); void markKeysAsPublished(); diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp index a327a643..a0386613 100644 --- a/lib/crypto/qolmsession.cpp +++ b/lib/crypto/qolmsession.cpp @@ -27,7 +27,7 @@ OlmSession* QOlmSession::create() return olm_session(new uint8_t[olm_session_size()]); } -std::variant, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) +std::variant QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) { if (preKeyMessage.type() != QOlmMessage::PreKey) { qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; @@ -53,17 +53,17 @@ std::variant, QOlmError> QOlmSession::createInbound return std::make_unique(olmSession); } -std::variant, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) +std::variant QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) { return createInbound(account, preKeyMessage); } -std::variant, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) +std::variant QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) { return createInbound(account, preKeyMessage, true, theirIdentityKey); } -std::variant, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +std::variant QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) { auto *olmOutboundSession = create(); const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); @@ -105,7 +105,7 @@ std::variant QOlmSession::pickle(const PicklingMode &mode return pickledBuf; } -std::variant, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +std::variant QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmSession = create(); diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h index 959c77d0..7a040b3d 100644 --- a/lib/crypto/qolmsession.h +++ b/lib/crypto/qolmsession.h @@ -74,7 +74,6 @@ private: OlmSession* m_session; }; - -//using QOlmSessionPtr = std::unique_ptr; +using QOlmSessionPtr = std::unique_ptr; } //namespace Quotient diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 84282dbf..ed6ad20b 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -38,7 +38,7 @@ public: EncryptionManager* q; // A map from senderKey to InboundSession - UnorderedMap> sessions; // TODO: cache + UnorderedMap sessions; void updateDeviceKeys( const QHash>& deviceKeys) @@ -76,7 +76,7 @@ public: qCWarning(E2EE) << "Failed to unpickle olm session"; continue; } - sessions[senderKey] = std::move(std::get>(sessionResult)); + sessions[senderKey] = std::move(std::get(sessionResult)); } } void saveSessions() { @@ -135,9 +135,11 @@ public: qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get(newSessionResult); return {}; } - std::unique_ptr newSession = std::move(std::get>(newSessionResult)); - // TODO Error handling? - olmAccount->removeOneTimeKeys(newSession); + auto newSession = std::move(std::get(newSessionResult)); + auto error = olmAccount->removeOneTimeKeys(newSession); + if (error) { + qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); + } const auto result = newSession->decrypt(message); sessions[senderKey] = std::move(newSession); saveSessions(); 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 9217026e46d7ac0d761cc5206d7ef00978558c47 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 28 Nov 2021 20:58:38 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- autotests/testolmsession.cpp | 2 +- lib/connection.cpp | 3 +-- lib/encryptionmanager.cpp | 2 +- lib/events/encryptedevent.cpp | 12 +++++------- lib/jobs/downloadfilejob.cpp | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 750b804e..00d76d4e 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -30,7 +30,7 @@ std::pair createSessionPair() throw "Wrong first message type received, can't create session"; } auto inbound = std::get(accountB.createInboundSession(preKey)); - return std::make_pair(std::move(inbound), std::move(outbound)); + return { std::move(inbound), std::move(outbound) }; } void TestOlmSession::olmOutboundSessionCreation() diff --git a/lib/connection.cpp b/lib/connection.cpp index a7af1477..ac428a62 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -56,7 +56,6 @@ #include #include - #if QT_VERSION_MAJOR >= 6 # include #else @@ -1948,7 +1947,7 @@ void Connection::Private::saveDevicesList() QElapsedTimer et; et.start(); - QFile outFile { q->e2eeDataDir() + QStringLiteral("/deviceslist.json") }; + QFile outFile { q->e2eeDataDir() % "/deviceslist.json" }; if (!outFile.open(QFile::WriteOnly)) { qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index ed6ad20b..5c106e12 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -80,7 +80,7 @@ public: } } void saveSessions() { - QFile outFile { static_cast(q->parent())->e2eeDataDir() + QStringLiteral("/olmsessions.json") }; + QFile outFile { static_cast(q->parent())->e2eeDataDir() % "/olmsessions.json" }; if (!outFile.open(QFile::WriteOnly)) { qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index c9257584..2e0d7387 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -38,17 +38,15 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const eventObject["event_id"] = id(); eventObject["sender"] = senderId(); eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); - if(contentJson().contains("m.relates_to")) { - auto relates = contentJson()["m.relates_to"].toObject(); + if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) { auto content = eventObject["content"].toObject(); - content["m.relates_to"] = relates; + content["m.relates_to"] = relatesToJson.toObject(); eventObject["content"] = content; } - if(unsignedJson().contains("redacts")) { - auto redacts = unsignedJson()["redacts"].toString(); + if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) { auto unsign = eventObject["unsigned"].toObject(); - unsign["redacts"] = redacts; + unsign["redacts"] = redactsJson.toString(); eventObject["unsigned"] = unsign; } - return makeEvent(eventObject); + return loadEvent(eventObject); } diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 0b4cf6d2..2eea9d59 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -8,7 +8,7 @@ #include #ifdef Quotient_E2EE_ENABLED -# include +# include # include "encryptionmanager.h" # include "events/encryptedfile.h" #endif -- 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/connection.cpp | 4 ++-- lib/events/event.h | 58 ++++++++++++++++++++++++++++++----------------------- lib/room.cpp | 9 +++++---- quotest/quotest.cpp | 4 ++-- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index e65fdac4..25219def 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -659,7 +659,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) // After running this loop, the account data events not saved in // accountData (see the end of the loop body) are auto-cleaned away for (auto&& eventPtr: accountDataEvents) { - visit(*eventPtr, + switchOnType(*eventPtr, [this](const DirectChatEvent& dce) { // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events const auto& usersToDCs = dce.usersToDirectChats(); @@ -760,7 +760,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) << ee.senderId(); // encryptionManager->updateDeviceKeys(); - visit(*sessionDecryptMessage(ee), + switchOnType(*sessionDecryptMessage(ee), [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); diff --git a/lib/events/event.h b/lib/events/event.h index 998a386c..ce737280 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -290,7 +290,7 @@ using Events = EventsArray; } \ // End of macro -// === is<>(), eventCast<>() and visit<>() === +// === is<>(), eventCast<>() and switchOnType<>() === template inline bool is(const Event& e) @@ -312,12 +312,12 @@ inline auto eventCast(const BasePtrT& eptr) : nullptr; } -// A single generic catch-all visitor +// A trivial generic catch-all "switch" template -inline auto visit(const BaseEventT& event, FnT&& visitor) - -> decltype(visitor(event)) +inline auto switchOnType(const BaseEventT& event, FnT&& fn) + -> decltype(fn(event)) { - return visitor(event); + return fn(event); } namespace _impl { @@ -328,52 +328,60 @@ namespace _impl { && !std::is_same_v>>; } -// A single type-specific void visitor +// A trivial type-specific "switch" for a void function template -inline auto visit(const BaseT& event, FnT&& visitor) +inline auto switchOnType(const BaseT& event, FnT&& fn) -> std::enable_if_t<_impl::needs_downcast && std::is_void_v>> { using event_type = fn_arg_t; if (is>(event)) - visitor(static_cast(event)); + fn(static_cast(event)); } -// A single type-specific non-void visitor with an optional default value -// non-voidness is guarded by defaultValue type +// A trivial type-specific "switch" for non-void functions with an optional +// default value; non-voidness is guarded by defaultValue type template -inline auto visit(const BaseT& event, FnT&& visitor, - fn_return_t&& defaultValue = {}) +inline auto switchOnType(const BaseT& event, FnT&& fn, + fn_return_t&& defaultValue = {}) -> std::enable_if_t<_impl::needs_downcast, fn_return_t> { using event_type = fn_arg_t; if (is>(event)) - return visitor(static_cast(event)); - return std::forward>(defaultValue); + return fn(static_cast(event)); + return std::move(defaultValue); } -// A chain of 2 or more visitors +// A switch for a chain of 2 or more functions template -inline std::common_type_t, fn_return_t> visit( - const BaseT& event, FnT1&& visitor1, FnT2&& visitor2, - FnTs&&... visitors) +inline std::common_type_t, fn_return_t> +switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) { using event_type1 = fn_arg_t; if (is>(event)) - return visitor1(static_cast(event)); - return visit(event, std::forward(visitor2), - std::forward(visitors)...); + return fn1(static_cast(event)); + return switchOnType(event, std::forward(fn2), + std::forward(fns)...); } -// A facility overload that calls void-returning visit() on each event +template +[[deprecated("The new name for visit() is switchOnType()")]] // +inline std::common_type_t...> +visit(const BaseT& event, FnTs&&... fns) +{ + return switchOnType(event, std::forward(fns)...); +} + + // A facility overload that calls void-returning switchOnType() on each event // over a range of event pointers +// TODO: replace with ranges::for_each once all standard libraries have it template -inline auto visitEach(RangeT&& events, FnTs&&... visitors) +inline auto visitEach(RangeT&& events, FnTs&&... fns) -> std::enable_if_t(visitors)...))>> + decltype(switchOnType(**begin(events), std::forward(fns)...))>> { for (auto&& evtPtr: events) - visit(*evtPtr, std::forward(visitors)...); + switchOnType(*evtPtr, std::forward(fns)...); } } // namespace Quotient Q_DECLARE_METATYPE(Quotient::Event*) 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 } diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index d006c7fb..764d5dfd 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -498,8 +498,8 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, clog << "File event " << txnId.toStdString() << " arrived in the timeline" << endl; - // This part tests visit() - return visit( + // This part tests switchOnType() + return switchOnType( *evt, [&](const RoomMessageEvent& e) { // TODO: check #366 once #368 is implemented -- cgit v1.2.3 From dc08fb9dfd474023084de9ce86f29f177ca52fdc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 2 Dec 2021 15:24:44 +0100 Subject: Improve function_traits<>; split out from util.* Quotient::function_traits<> did not support member functions in a proper way (i.e. the way std::invoke_result<> treats them, with the function's owning class represented as the first parameter). Now that I gained the skill and understanding in function_traits<> somewhat wicked machinery, I could properly support member functions. Overloads and generic lambdas are not supported but maybe we'll get to those one day. --- CMakeLists.txt | 2 ++ lib/events/event.h | 1 + lib/function_traits.cpp | 53 +++++++++++++++++++++++++++ lib/function_traits.h | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/qt_connection_util.h | 2 +- lib/util.cpp | 32 ----------------- lib/util.h | 52 --------------------------- 7 files changed, 150 insertions(+), 85 deletions(-) create mode 100644 lib/function_traits.cpp create mode 100644 lib/function_traits.h diff --git a/CMakeLists.txt b/CMakeLists.txt index aa3b9c98..ca92699c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,8 @@ endif () # Set up source files list(APPEND lib_SRCS lib/quotient_common.h + lib/function_traits.h + lib/function_traits.cpp lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp diff --git a/lib/events/event.h b/lib/events/event.h index ce737280..4d4bb16b 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -5,6 +5,7 @@ #include "converters.h" #include "logging.h" +#include "function_traits.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp new file mode 100644 index 00000000..4ff427e4 --- /dev/null +++ b/lib/function_traits.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "function_traits.h" + +// Tests for function_traits<> + +using namespace Quotient; + +int f_(); +static_assert(std::is_same, int>::value, + "Test fn_return_t<>"); + +void f1_(int, float); +static_assert(std::is_same, float>::value, + "Test fn_arg_t<>"); + +struct Fo { + int operator()(); + static constexpr auto l = [] { return 0.0f; }; + bool memFn(); + void constMemFn() const&; + double field; + const double field2; +}; +static_assert(std::is_same_v, int>, + "Test return type of function object"); +static_assert(std::is_same_v, float>, + "Test return type of lambda"); +static_assert(std::is_same_v, Fo>, + "Test first argument type of member function"); +static_assert(std::is_same_v, bool>, + "Test return type of member function"); +static_assert(std::is_same_v, const Fo&>, + "Test first argument type of const member function"); +static_assert(std::is_void_v>, + "Test return type of const member function"); +static_assert(std::is_same_v, double&>, + "Test return type of a class member"); +static_assert(std::is_same_v, const double&>, + "Test return type of a const class member"); + +struct Fo1 { + void operator()(int); +}; +static_assert(std::is_same, int>(), + "Test fn_arg_t defaulting to first argument"); + +template +static void ft(const std::vector&); +static_assert( + std::is_same)>, const std::vector&>(), + "Test function templates"); diff --git a/lib/function_traits.h b/lib/function_traits.h new file mode 100644 index 00000000..83b8e425 --- /dev/null +++ b/lib/function_traits.h @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +namespace Quotient { + +namespace _impl { + template + struct fn_traits {}; +} + +/// Determine traits of an arbitrary function/lambda/functor +/*! + * Doesn't work with generic lambdas and function objects that have + * operator() overloaded. + * \sa + * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 + */ +template +struct function_traits + : public _impl::fn_traits> {}; + +// Specialisation for a function +template +struct function_traits { + using return_type = ReturnT; + using arg_types = std::tuple; + // See also the comment for wrap_in_function() in qt_connection_util.h + using function_type = std::function; +}; + +namespace _impl { + template + struct fn_object_traits; + + // Specialisation for a lambda function + template + struct fn_object_traits + : function_traits {}; + + // Specialisation for a const lambda function + template + struct fn_object_traits + : function_traits {}; + + // Specialisation for function objects with (non-overloaded) operator() + // (this includes non-generic lambdas) + template + struct fn_traits + : public fn_object_traits {}; + + // Specialisation for a member function in a non-functor class + template + struct fn_traits + : function_traits {}; + + // Specialisation for a const member function + template + struct fn_traits + : function_traits {}; + + // Specialisation for a constref member function + template + struct fn_traits + : function_traits {}; + + // Specialisation for a prvalue member function + template + struct fn_traits + : function_traits {}; + + // Specialisation for a pointer-to-member + template + struct fn_traits + : function_traits {}; + + // Specialisation for a const pointer-to-member + template + struct fn_traits + : function_traits {}; +} // namespace _impl + +template +using fn_return_t = typename function_traits::return_type; + +template +using fn_arg_t = + std::tuple_element_t::arg_types>; + +} // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 46294499..86593cc8 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -3,7 +3,7 @@ #pragma once -#include "util.h" +#include "function_traits.h" #include diff --git a/lib/util.cpp b/lib/util.cpp index 2dfb09a6..03ebf325 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -135,35 +135,3 @@ int Quotient::patchVersion() { return Quotient_VERSION_PATCH; } - -// Tests for function_traits<> - -using namespace Quotient; - -int f_(); -static_assert(std::is_same, int>::value, - "Test fn_return_t<>"); - -void f1_(int, QString); -static_assert(std::is_same, QString>::value, - "Test fn_arg_t<>"); - -struct Fo { - int operator()(); - static constexpr auto l = [] { return 0.0f; }; -}; -static_assert(std::is_same, int>::value, - "Test return type of function object"); -static_assert(std::is_same, float>::value, - "Test return type of lambda"); - -struct Fo1 { - void operator()(int); -}; -static_assert(std::is_same, int>(), - "Test fn_arg_t defaulting to first argument"); - -template -static QString ft(T&&); -static_assert(std::is_same)>, QString&&>(), - "Test function templates"); diff --git a/lib/util.h b/lib/util.h index 13efb94b..97f0ecbc 100644 --- a/lib/util.h +++ b/lib/util.h @@ -189,58 +189,6 @@ inline auto merge(T1& lhs, const Omittable& rhs) return true; } -namespace _impl { - template - struct fn_traits {}; -} - -/// Determine traits of an arbitrary function/lambda/functor -/*! - * Doesn't work with generic lambdas and function objects that have - * operator() overloaded. - * \sa - * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 - */ -template -struct function_traits - : public _impl::fn_traits> {}; - -// Specialisation for a function -template -struct function_traits { - using return_type = ReturnT; - using arg_types = std::tuple; - // Doesn't (and there's no plan to make it) work for "classic" - // member functions (i.e. outside of functors). - // See also the comment for wrap_in_function() below - using function_type = std::function; -}; - -namespace _impl { - // Specialisation for function objects with (non-overloaded) operator() - // (this includes non-generic lambdas) - template - struct fn_traits - : public fn_traits {}; - - // Specialisation for a member function - template - struct fn_traits - : function_traits {}; - - // Specialisation for a const member function - template - struct fn_traits - : function_traits {}; -} // namespace _impl - -template -using fn_return_t = typename function_traits::return_type; - -template -using fn_arg_t = - std::tuple_element_t::arg_types>; - inline constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); -- cgit v1.2.3 From c3a0515ac2ead7b2d0653be3517836cd31be480e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 2 Dec 2021 21:18:30 +0100 Subject: Cleanup on Sonar issues --- lib/converters.h | 4 ++-- lib/function_traits.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index 90349019..9c3d5749 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -27,8 +27,8 @@ struct JsonObjectConverter { //! \brief The switchboard for extra conversion algorithms behind from/toJson //! //! This template is mainly intended for partial conversion specialisations -//! since from/toJson are functions and cannot be partially specialised; -//! another case for JsonConverter is to insulate types that can be constructed +//! since from/toJson are functions and cannot be partially specialised. +//! Another case for JsonConverter is to insulate types that can be constructed //! from basic types - namely, QVariant and QUrl can be directly constructed //! from QString and having an overload or specialisation for those leads to //! ambiguity between these and QJsonValue. For trivial (converting diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp index 4ff427e4..20bcf30e 100644 --- a/lib/function_traits.cpp +++ b/lib/function_traits.cpp @@ -8,11 +8,11 @@ using namespace Quotient; int f_(); -static_assert(std::is_same, int>::value, +static_assert(std::is_same_v, int>, "Test fn_return_t<>"); void f1_(int, float); -static_assert(std::is_same, float>::value, +static_assert(std::is_same_v, float>, "Test fn_arg_t<>"); struct Fo { @@ -43,7 +43,7 @@ static_assert(std::is_same_v, const double&>, struct Fo1 { void operator()(int); }; -static_assert(std::is_same, int>(), +static_assert(std::is_same_v, int>, "Test fn_arg_t defaulting to first argument"); template -- cgit v1.2.3 From 3b5d5f65c5eb10997362f9f9509fe2161eaa335c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 3 Dec 2021 15:07:22 +0100 Subject: CMakeLists.txt: add .h files to sources This is driven by the change in the way Qt Creator 6 works with CMake (see https://www.qt.io/blog/qt-creator-6-cmake-update) that basically requires you to explicitly add header files as source files. While this obviously added to the size of the source files list, it also drove dropping the repeated file(GLOB_RECURSE ... CONFIGURE_DEPENDS) call which added a considerable speedbump to the beginning of each build (now that call is just one). --- CMakeLists.txt | 131 ++++++++++++++++++++++++++------------------------------- 1 file changed, 60 insertions(+), 71 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ca92699c..d1dcc106 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,54 +125,53 @@ endif () # Set up source files list(APPEND lib_SRCS lib/quotient_common.h - lib/function_traits.h - lib/function_traits.cpp - lib/networkaccessmanager.cpp - lib/connectiondata.cpp - lib/connection.cpp - lib/ssosession.cpp - lib/logging.cpp - lib/room.cpp - lib/user.cpp - lib/avatar.cpp - lib/uri.cpp - lib/uriresolver.cpp - lib/eventstats.cpp - lib/syncdata.cpp - lib/settings.cpp - lib/networksettings.cpp - lib/converters.cpp - lib/util.cpp - lib/encryptionmanager.cpp - lib/eventitem.cpp - lib/accountregistry.cpp - lib/mxcreply.cpp - lib/events/event.cpp - lib/events/roomevent.cpp - lib/events/stateevent.cpp - lib/events/eventcontent.cpp - lib/events/roomcreateevent.cpp - lib/events/roomtombstoneevent.cpp - lib/events/roommessageevent.cpp - lib/events/roommemberevent.cpp - lib/events/roompowerlevelsevent.cpp - lib/events/typingevent.cpp - lib/events/receiptevent.cpp - lib/events/reactionevent.cpp - lib/events/callanswerevent.cpp - lib/events/callcandidatesevent.cpp - lib/events/callhangupevent.cpp - lib/events/callinviteevent.cpp - lib/events/directchatevent.cpp - lib/events/encryptionevent.cpp - lib/events/encryptedevent.cpp - lib/events/roomkeyevent.cpp - lib/events/stickerevent.cpp - lib/jobs/requestdata.cpp - lib/jobs/basejob.cpp - lib/jobs/syncjob.cpp - lib/jobs/mediathumbnailjob.cpp - lib/jobs/downloadfilejob.cpp + lib/function_traits.h lib/function_traits.cpp + lib/networkaccessmanager.h lib/networkaccessmanager.cpp + lib/connectiondata.h lib/connectiondata.cpp + lib/connection.h lib/connection.cpp + lib/ssosession.h lib/ssosession.cpp + lib/logging.h lib/logging.cpp + lib/room.h lib/room.cpp + lib/user.h lib/user.cpp + lib/avatar.h lib/avatar.cpp + lib/uri.h lib/uri.cpp + lib/uriresolver.h lib/uriresolver.cpp + lib/eventstats.h lib/eventstats.cpp + lib/syncdata.h lib/syncdata.cpp + lib/settings.h lib/settings.cpp + lib/networksettings.h lib/networksettings.cpp + lib/converters.h lib/converters.cpp + lib/util.h lib/util.cpp + lib/encryptionmanager.h lib/encryptionmanager.cpp + lib/eventitem.h lib/eventitem.cpp + lib/accountregistry.h lib/accountregistry.cpp + lib/mxcreply.h lib/mxcreply.cpp + lib/events/event.h lib/events/event.cpp + lib/events/roomevent.h lib/events/roomevent.cpp + lib/events/stateevent.h lib/events/stateevent.cpp + lib/events/eventcontent.h lib/events/eventcontent.cpp + lib/events/roomcreateevent.h lib/events/roomcreateevent.cpp + lib/events/roomtombstoneevent.h lib/events/roomtombstoneevent.cpp + lib/events/roommessageevent.h lib/events/roommessageevent.cpp + lib/events/roommemberevent.h lib/events/roommemberevent.cpp + lib/events/roompowerlevelsevent.h lib/events/roompowerlevelsevent.cpp + lib/events/typingevent.h lib/events/typingevent.cpp + lib/events/receiptevent.h lib/events/receiptevent.cpp + lib/events/reactionevent.h lib/events/reactionevent.cpp + lib/events/callanswerevent.h lib/events/callanswerevent.cpp + lib/events/callcandidatesevent.h lib/events/callcandidatesevent.cpp + lib/events/callhangupevent.h lib/events/callhangupevent.cpp + lib/events/callinviteevent.h lib/events/callinviteevent.cpp + lib/events/directchatevent.h lib/events/directchatevent.cpp + lib/events/encryptionevent.h lib/events/encryptionevent.cpp + lib/events/encryptedevent.h lib/events/encryptedevent.cpp + lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp + lib/events/stickerevent.h lib/events/stickerevent.cpp + lib/jobs/requestdata.h lib/jobs/requestdata.cpp + lib/jobs/basejob.h lib/jobs/basejob.cpp + lib/jobs/syncjob.h lib/jobs/syncjob.cpp + lib/jobs/mediathumbnailjob.h lib/jobs/mediathumbnailjob.cpp + lib/jobs/downloadfilejob.h lib/jobs/downloadfilejob.cpp ) # Configure API files generation @@ -217,21 +216,13 @@ if (API_GENERATION_ENABLED) message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") endif () - # We use globbing with CONFIGURE_DEPENDS to produce two file lists: - # one of all API files for clang-format and another of just .cpp - # files to supply for library source files. Since we expect these - # file lists to only change due to GTAD invocation, we only use - # CONFIGURE_DEPENDS when pre-requisites to update API are met. - # Read comments next to each file(GLOB_RECURSE) for caveats. - set(add_CONFIGURE_DEPENDS "CONFIGURE_DEPENDS") - set(FULL_CSAPI_SRC_DIR ${ABS_API_DEF_PATH}/client-server) file(GLOB_RECURSE API_DEFS RELATIVE ${PROJECT_SOURCE_DIR} ${FULL_CSAPI_SRC_DIR}/*.yaml ${ABS_API_DEF_PATH}/${ASAPI_DEF_DIR}/*.yaml ${ABS_API_DEF_PATH}/${ISAPI_DEF_DIR}/*.yaml ) - add_custom_target(generate-unformatted-api + add_custom_target(update-api ${ABS_GTAD_PATH} --config ../gtad/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} old_sync.yaml- room_initial_sync.yaml- # deprecated @@ -271,21 +262,19 @@ if (API_GENERATION_ENABLED) endif() add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" "build target update-api") -if (API_GENERATION_ENABLED) - add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" - "formatting of generated API files with clang-format") -endif() - -# Make no mistake: CMake cannot run gtad first and then populate the list of -# resulting api_SRCS files. In other words, placing the below statement after -# the block above will not lead to CMake magically reconfiguring the build -# after building the update-api target. CONFIGURE_DEPENDS somewhat helps, -# at least forcing the build system to reevaluate the glob before building -# the next target. Otherwise, you have to watch out if gtad has created -# new files and if it has, re-run cmake. -file(GLOB_RECURSE api_SRCS ${add_CONFIGURE_DEPENDS} ${FULL_CSAPI_DIR}/*.cpp) -add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) +# Produce the list of all Matrix API files for building the library. When this +# list changes (normally after calling GTAD), CONFIGURE_DEPENDS will force +# the build system to call CMake again. Checking for the glob change slows down +# each build (even if the target does not involve API generation). It would be +# ideal if GTAD could compare the initial (saved somewhere) and the generated +# file list itself and write down to some .cmake file if those are different, +# which would trigger the reconfiguration specifically before the next build. +# For now CONFIGURE_DEPENDS is the best approximation of that. +file(GLOB_RECURSE api_ALL_SRCS CONFIGURE_DEPENDS + ${FULL_CSAPI_DIR}/*.* lib/${ASAPI_DEF_DIR}/*.* lib/${ISAPI_DEF_DIR}/*.*) + +add_library(${PROJECT_NAME} ${lib_SRCS} ${api_ALL_SRCS}) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} -- cgit v1.2.3 From dc458f3952b0005a0feb63fbebed11ae9ad5a062 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 3 Dec 2021 15:11:32 +0100 Subject: Rely on GTAD 0.9 that calls clang-format itself There was that ugly workaround in CMakeLists.txt to produce the list of files to be formatted and making a separate build target for clang-format. As GTAD 0.9 calls clang-format itself this workaround is no more necessary; generate-unformatted-api and format-api build targets are now gone, and clang-format is no more one-build-system-configuration behind GTAD on the list of files to handle. --- CMakeLists.txt | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d1dcc106..8d9be635 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,7 +182,6 @@ set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) set(API_GENERATION_ENABLED 0) -set(API_FORMATTING_ENABLED 0) if (GTAD_PATH AND MATRIX_DOC_PATH) # REALPATH resolves ~ (home directory) while PROGRAM doesn't get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) @@ -209,11 +208,8 @@ if (API_GENERATION_ENABLED) set(CLANG_FORMAT clang-format) endif() get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM PROGRAM_ARGS CLANG_FORMAT_ARGS) - if (ABS_CLANG_FORMAT) - set(API_FORMATTING_ENABLED 1) - message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") - else () - message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") + if (NOT ABS_CLANG_FORMAT) + message( WARNING "${CLANG_FORMAT} is NOT FOUND; API files won't be formatted") endif () set(FULL_CSAPI_SRC_DIR ${ABS_API_DEF_PATH}/client-server) @@ -237,28 +233,6 @@ if (API_GENERATION_ENABLED) ${API_DEFS} VERBATIM ) - add_custom_target(update-api DEPENDS generate-unformatted-api) - if (EXISTS ${ABS_CLANG_FORMAT}) - set(CLANG_FORMAT_ARGS -i -sort-includes ${CLANG_FORMAT_ARGS}) - # FIXME: the list of files should be produced after GTAD has run. - # For now it's produced at CMake invocation. If file() hasn't found - # any files at that moment, clang-format will be called with an empty - # list of files and choke. Taking outfiles.txt from GTAD could be - # an option but this, too, must be done during the build stage, and - # there's no crossplatform way to use the contents of a file as - # input for a build target command. - file(GLOB_RECURSE api_ALL_SRCS ${add_CONFIGURE_DEPENDS} - ${FULL_CSAPI_DIR}/*.* - lib/${ASAPI_DEF_DIR}/*.* - lib/${ISAPI_DEF_DIR}/*.*) - if (api_ALL_SRCS) - add_custom_target(format-api - ${ABS_CLANG_FORMAT} ${CLANG_FORMAT_ARGS} ${api_ALL_SRCS} - DEPENDS generate-unformatted-api - VERBATIM) - add_dependencies(update-api format-api) - endif() - endif() endif() add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" "build target update-api") -- cgit v1.2.3 From 0d212bdb7849d12582d906a03935cb6f51767e3b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 3 Dec 2021 21:22:46 +0100 Subject: Add a couple of bare .h files to CMakeLists.txt These were missing from the commit introducing .h files to sources, highlighting the reason why .h files should be there... --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d9be635..2762df6a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,13 +149,16 @@ list(APPEND lib_SRCS lib/events/event.h lib/events/event.cpp lib/events/roomevent.h lib/events/roomevent.cpp lib/events/stateevent.h lib/events/stateevent.cpp + lib/events/simplestateevents.h lib/events/eventcontent.h lib/events/eventcontent.cpp lib/events/roomcreateevent.h lib/events/roomcreateevent.cpp lib/events/roomtombstoneevent.h lib/events/roomtombstoneevent.cpp lib/events/roommessageevent.h lib/events/roommessageevent.cpp lib/events/roommemberevent.h lib/events/roommemberevent.cpp + lib/events/roomavatarevent.h lib/events/roompowerlevelsevent.h lib/events/roompowerlevelsevent.cpp lib/events/typingevent.h lib/events/typingevent.cpp + lib/events/accountdataevents.h lib/events/receiptevent.h lib/events/receiptevent.cpp lib/events/reactionevent.h lib/events/reactionevent.cpp lib/events/callanswerevent.h lib/events/callanswerevent.cpp -- cgit v1.2.3 From 776d05bf98a5dd9e484d5a0e651c71fa95498689 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 3 Dec 2021 21:36:32 +0100 Subject: Cleanup; drop an unused RoomAliasesEvent constructor Also, RoomAliasesEvent is to be completely gone after 0.7. --- lib/events/accountdataevents.h | 5 ++--- lib/events/simplestateevents.h | 12 ++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index e5101d20..9cf77be3 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -4,7 +4,6 @@ #pragma once #include "event.h" -#include "eventcontent.h" namespace Quotient { constexpr const char* FavouriteTag = "m.favourite"; @@ -16,12 +15,12 @@ struct TagRecord { order_type order; - TagRecord(order_type order = none) : order(std::move(order)) {} + TagRecord(order_type order = none) : order(order) {} bool operator<(const TagRecord& other) const { // Per The Spec, rooms with no order should be after those with order, - // against optional<>::operator<() convention. + // against std::optional<>::operator<() convention. return order && (!other.order || *order < *other.order); } }; diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index cf1bfbba..9ce78609 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -8,8 +8,7 @@ namespace Quotient { namespace EventContent { template - class SimpleContent { - public: + struct SimpleContent { using value_type = T; // The constructor is templated to enable perfect forwarding @@ -25,11 +24,8 @@ namespace EventContent { return { { key, Quotient::toJson(value) } }; } - public: T value; - - protected: - QString key; + const QString key; }; } // namespace EventContent @@ -65,10 +61,6 @@ public: explicit RoomAliasesEvent(const QJsonObject& obj) : StateEvent(typeId(), obj, QStringLiteral("aliases")) {} - RoomAliasesEvent(const QString& server, const QStringList& aliases) - : StateEvent(typeId(), matrixTypeId(), server, - QStringLiteral("aliases"), aliases) - {} QString server() const { return stateKey(); } QStringList aliases() const { return content().value; } }; -- cgit v1.2.3 From 7981c654c0cd53f6e100ecae25882246a9de5d6e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 5 Dec 2021 21:47:25 +0100 Subject: Drop 'qmc-example' from one last(?) place --- quotest/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index 59334e30..8d67b6f1 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -8,7 +8,7 @@ find_package(${Qt} COMPONENTS Concurrent) add_executable(quotest ${quotest_SRCS}) target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME}) -option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) +option(${PROJECT_NAME}_INSTALL_TESTS "install quotest application" ON) add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS "the library functional test suite") -- cgit v1.2.3 From 9ddb72611f5575ccd5d3b68734a9d40d8ef2befc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Dec 2021 07:17:17 +0100 Subject: Add execution of autotests to CI --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47e55421..ec6b671c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: - name: Build and install libQuotient run: | - cmake --build build --target install + cmake --build build --target all install ls ~/.local/$BIN_DIR/quotest - name: Run tests @@ -164,6 +164,8 @@ jobs: QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | + cd build + ctest --output-on-failure [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From ac0ce7da24b4e567a34863db3261f755674d8337 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Dec 2021 22:42:33 +0100 Subject: autotests/: don't instantiate QApplication Those tests don't even need an event loop. --- autotests/callcandidateseventtest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index 1a69cb66..0d5a543b 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -53,5 +53,5 @@ void TestCallCandidatesEvent::fromJson() QStringLiteral("candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0")); } -QTEST_MAIN(TestCallCandidatesEvent) +QTEST_APPLESS_MAIN(TestCallCandidatesEvent) #include "callcandidateseventtest.moc" -- 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 --- CMakeLists.txt | 5 +- lib/connection.cpp | 23 ++-- lib/crypto/e2ee.h | 6 + lib/crypto/qolminboundsession.cpp | 2 +- lib/crypto/qolminboundsession.h | 2 +- lib/crypto/qolmsession.h | 3 - lib/database.cpp | 240 ++++++++++++++++++++++++++++++++++++++ lib/database.h | 46 ++++++++ lib/encryptionmanager.cpp | 96 +++------------ lib/events/encryptedevent.cpp | 1 + lib/logging.h | 1 + lib/room.cpp | 82 ++----------- 12 files changed, 335 insertions(+), 172 deletions(-) create mode 100644 lib/database.cpp create mode 100644 lib/database.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dbb43f89..9f886094 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,7 @@ else() set(QtExtraModules "Multimedia") # See #483 endif() string(REGEX REPLACE "^(.).*" "Qt\\1" Qt ${QtMinVersion}) # makes "Qt5" or "Qt6" -find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModules}) +find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test Sql ${QtExtraModules}) get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") @@ -133,6 +133,7 @@ list(APPEND lib_SRCS lib/eventitem.cpp lib/accountregistry.cpp lib/mxcreply.cpp + lib/database.cpp lib/events/event.cpp lib/events/roomevent.cpp lib/events/stateevent.cpp @@ -327,7 +328,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) find_dependency(OpenSSL)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES}) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${Qt}::Sql ${QTKEYCHAIN_LIBRARIES}) if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) diff --git a/lib/connection.cpp b/lib/connection.cpp index ac428a62..f344807e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -62,6 +62,8 @@ # include #endif +#include "database.h" + using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() @@ -274,6 +276,7 @@ Connection::Connection(const QUrl& server, QObject* parent) }); #endif d->q = this; // All d initialization should occur before this line + Database::instance(); } Connection::Connection(QObject* parent) : Connection({}, parent) {} @@ -439,6 +442,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) auto loginJob = q->callApi(std::forward(loginArgs)...); connect(loginJob, &BaseJob::success, q, [this, loginJob] { + Database::instance().clear(loginJob->userId()); data->setToken(loginJob->accessToken().toLatin1()); data->setDeviceId(loginJob->deviceId()); completeSetup(loginJob->userId()); @@ -504,7 +508,7 @@ void Connection::Private::completeSetup(const QString& mxId) encryptionManager = new EncryptionManager(q); - if (accountSettings.encryptionAccountPickle().isEmpty()) { + if (Database::instance().accountPickle(data->userId()).isEmpty()) { // create new account and save unpickle data olmAccount->createNewAccount(); auto job = q->callApi(olmAccount->deviceKeys()); @@ -513,7 +517,7 @@ void Connection::Private::completeSetup(const QString& mxId) }); } else { // account already existing - auto pickle = accountSettings.encryptionAccountPickle(); + auto pickle = Database::instance().accountPickle(data->userId()); olmAccount->unpickle(pickle, picklingMode); } #endif // Quotient_E2EE_ENABLED @@ -1978,15 +1982,9 @@ void Connection::Private::saveDevicesList() rootObj.insert(QStringLiteral("sync_token"), q->nextBatchToken()); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - const auto data = - cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() - : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); -#else + QJsonDocument json { rootObj }; - const auto data = cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif + const auto data = json.toJson(); qCDebug(PROFILER) << "DeviceList generated in" << et; outFile.write(data.data(), data.size()); @@ -2043,11 +2041,10 @@ PicklingMode Connection::picklingMode() const void Connection::saveOlmAccount() { - qCDebug(E2EE) << "Saving olm account"; + qDebug() << "Saving olm account"; #ifdef Quotient_E2EE_ENABLED auto pickle = d->olmAccount->pickle(d->picklingMode); - AccountSettings(d->data->userId()).setEncryptionAccountPickle(std::get(pickle)); - //TODO handle errors + Database::instance().setAccountPickle(userId(), std::get(pickle)); #endif } diff --git a/lib/crypto/e2ee.h b/lib/crypto/e2ee.h index 2d280185..41cd2878 100644 --- a/lib/crypto/e2ee.h +++ b/lib/crypto/e2ee.h @@ -49,6 +49,12 @@ struct Encrypted { using PicklingMode = std::variant; +class QOlmSession; +using QOlmSessionPtr = std::unique_ptr; + +class QOlmInboundGroupSession; +using QOlmInboundGroupSessionPtr = std::unique_ptr; + template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; diff --git a/lib/crypto/qolminboundsession.cpp b/lib/crypto/qolminboundsession.cpp index beaf3299..31d699f1 100644 --- a/lib/crypto/qolminboundsession.cpp +++ b/lib/crypto/qolminboundsession.cpp @@ -72,7 +72,7 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const return pickledBuf; } -std::variant, QOlmError> QOlmInboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +std::variant, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); diff --git a/lib/crypto/qolminboundsession.h b/lib/crypto/qolminboundsession.h index 36ab4942..362e42ba 100644 --- a/lib/crypto/qolminboundsession.h +++ b/lib/crypto/qolminboundsession.h @@ -27,7 +27,7 @@ public: QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant, QOlmError> unpickle(QByteArray &picked, const PicklingMode &mode); + static std::variant, QOlmError> unpickle(const QByteArray &picked, const PicklingMode &mode); //! Decrypts ciphertext received for this group session. std::variant, QOlmError> decrypt(const QByteArray &message); //! Export the base64-encoded ratchet key for this session, at the given index, diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h index 7a040b3d..711ca66b 100644 --- a/lib/crypto/qolmsession.h +++ b/lib/crypto/qolmsession.h @@ -73,7 +73,4 @@ private: static std::variant, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); OlmSession* m_session; }; - -using QOlmSessionPtr = std::unique_ptr; - } //namespace Quotient diff --git a/lib/database.cpp b/lib/database.cpp new file mode 100644 index 00000000..153aab31 --- /dev/null +++ b/lib/database.cpp @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "database.h" + +#include +#include +#include +#include +#include +#include + +#include "crypto/e2ee.h" +#include "crypto/qolmsession.h" +#include "crypto/qolminboundsession.h" + +//TODO: delete room specific data when leaving room + +using namespace Quotient; +Database::Database() +{ + QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QDir(databasePath).mkpath(databasePath); + QSqlDatabase::database().setDatabaseName(databasePath + QStringLiteral("/database.db3")); + QSqlDatabase::database().open(); + + switch(version()) { + case 0: migrateTo1(); + } +} + +int Database::version() +{ + auto query = execute(QStringLiteral("PRAGMA user_version;")); + if (query.next()) { + bool ok; + int value = query.value(0).toInt(&ok); + qDebug() << "Database version" << value; + if (ok) + return value; + } else { + qCritical() << "Failed to check database version"; + } + return -1; +} + +QSqlQuery Database::execute(const QString &queryString) +{ + auto query = QSqlDatabase::database().exec(queryString); + if (query.lastError().type() != QSqlError::NoError) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +QSqlQuery Database::execute(QSqlQuery &query) +{ + if (!query.exec()) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +void Database::transaction() +{ + QSqlDatabase::database().transaction(); +} + +void Database::commit() +{ + QSqlDatabase::database().commit(); +} + +void Database::migrateTo1() +{ + qDebug() << "Migrating database to version 1"; + transaction(); + execute(QStringLiteral("CREATE TABLE Accounts (matrixId TEXT UNIQUE, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE OlmSessions (matrixId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE InboundMegolmSessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE OutboundMegolmSessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE GroupSessionIndexRecord (matrixId TEXT, roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("PRAGMA user_version = 1;")); + commit(); +} + +QByteArray Database::accountPickle(const QString &id) +{ + QSqlQuery query; + query.prepare(QStringLiteral("SELECT pickle FROM Accounts WHERE matrixId=:matrixId;")); + query.bindValue(":matrixId", id); + execute(query); + if (query.next()) { + return query.value(QStringLiteral("pickle")).toByteArray(); + } + return {}; +} + +void Database::setAccountPickle(const QString &id, const QByteArray &pickle) +{ + QSqlQuery query; + query.prepare(QStringLiteral("INSERT INTO Accounts(matrixId, pickle) VALUES(:matrixId, :pickle) ON CONFLICT (matrixId) DO UPDATE SET pickle=:pickle WHERE matrixId=:matrixId;")); + query.bindValue(":matrixId", id); + query.bindValue(":pickle", pickle); + transaction(); + execute(query); + commit(); +} + +void Database::clear(const QString &id) +{ + QSqlQuery query; + query.prepare(QStringLiteral("DELETE FROM Accounts(matrixId, pickle) WHERE matrixId=:matrixId;")); + query.bindValue(":matrixId", id); + + QSqlQuery sessionsQuery; + sessionsQuery.prepare(QStringLiteral("DELETE FROM OlmSessions WHERE matrixId=:matrixId;")); + sessionsQuery.bindValue(":matrixId", id); + + QSqlQuery megolmSessionsQuery; + megolmSessionsQuery.prepare(QStringLiteral("DELETE FROM InboundMegolmSessions WHERE matrixId=:matrixId;")); + megolmSessionsQuery.bindValue(":matrixId", id); + + QSqlQuery groupSessionIndexRecordQuery; + groupSessionIndexRecordQuery.prepare(QStringLiteral("DELETE FROM GroupSessionIndexRecord WHERE matrixId=:matrixId;")); + groupSessionIndexRecordQuery.bindValue(":matrixId", matrixId); + + transaction(); + execute(query); + execute(sessionsQuery); + execute(megolmSessionsQuery); + execute(groupSessionIndexRecordQuery); + commit(); + +} + +void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle) +{ + QSqlQuery query; + query.prepare(QStringLiteral("INSERT INTO OlmSessions(matrixId, senderKey, sessionId, pickle) VALUES(:matrixId, :senderKey, :sessionId, :pickle);")); + query.bindValue(":matrixId", matrixId); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + transaction(); + execute(query); + commit(); +} + +UnorderedMap> Database::loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode) +{ + QSqlQuery query; + query.prepare(QStringLiteral("SELECT * FROM OlmSessions WHERE matrixId=:matrixId;")); + query.bindValue(":matrixId", matrixId); + transaction(); + execute(query); + commit(); + UnorderedMap> sessions; + while (query.next()) { + auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative(session)) { + qCWarning(E2EE) << "Failed to unpickle olm session"; + continue; + } + sessions[query.value("senderKey").toString()].push_back(std::move(std::get(session))); + } + return sessions; +} + +UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode) +{ + QSqlQuery query; + query.prepare(QStringLiteral("SELECT * FROM InboundMegolmSessions WHERE matrixId=:matrixId AND roomId=:roomId;")); + query.bindValue(":matrixId", matrixId); + query.bindValue(":roomId", roomId); + transaction(); + execute(query); + commit(); + UnorderedMap, QOlmInboundGroupSessionPtr> sessions; + while (query.next()) { + auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative(session)) { + qCWarning(E2EE) << "Failed to unpickle megolm session"; + continue; + } + sessions[{query.value("senderKey").toString(), query.value("sessionId").toString()}] = std::move(std::get(session)); + } + return sessions; +} + +void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + QSqlQuery query; + query.prepare(QStringLiteral("INSERT INTO InboundMegolmSessions(matrixId, roomId, senderKey, sessionId, pickle) VALUES(:matrixId, :roomId, :senderKey, :sessionId, :pickle);")); + query.bindValue(":matrixId", matrixId); + query.bindValue(":roomId", roomId); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + transaction(); + execute(query); + commit(); +} + +void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) +{ + QSqlQuery query; + query.prepare("INSERT INTO GroupSessionIndexRecord(matrixId, roomId, sessionId, i, eventId, ts) VALUES(:matrixId, :roomId, :sessionId, :index, :eventId, :ts);"); + query.bindValue(":matrixId", matrixId); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + query.bindValue(":eventId", eventId); + query.bindValue(":ts", ts); + transaction(); + execute(query); + commit(); +} + +QPair Database::groupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, qint64 index) +{ + QSqlQuery query; + query.prepare(QStringLiteral("SELECT * FROM GroupSessionIndexRecord WHERE matrixId=:matrixId AND roomId=:roomId AND sessionId=:sessionId AND i=:index;")); + query.bindValue(":matrixId", matrixId); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + transaction(); + execute(query); + commit(); + if (!query.next()) { + return {}; + } + return {query.value("eventId").toString(), query.value("ts").toLongLong()}; +} diff --git a/lib/database.h b/lib/database.h new file mode 100644 index 00000000..ed356820 --- /dev/null +++ b/lib/database.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include + +#include "crypto/e2ee.h" + +namespace Quotient { +class Database : public QObject +{ + Q_OBJECT + +public: + static Database &instance() + { + static Database _instance; + return _instance; + } + + int version(); + void transaction(); + void commit(); + QSqlQuery execute(const QString &queryString); + QSqlQuery execute(QSqlQuery &query); + + QByteArray accountPickle(const QString &id); + void setAccountPickle(const QString &id, const QByteArray &pickle); + void clear(const QString &id); + void saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle); + UnorderedMap> loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode); + UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& matrixId, const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); + void addGroupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); + QPair groupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, qint64 index); + + +private: + Database(); + + void migrateTo1(); +}; +} diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 5c106e12..e5fa978f 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -8,6 +8,7 @@ #include "connection.h" #include "crypto/e2ee.h" #include "events/encryptedfile.h" +#include "database.h" #include "csapi/keys.h" @@ -37,90 +38,28 @@ public: EncryptionManager* q; - // A map from senderKey to InboundSession - UnorderedMap sessions; - void updateDeviceKeys( - const QHash>& deviceKeys) - { - for (auto userId : deviceKeys.keys()) { - for (auto deviceId : deviceKeys.value(userId).keys()) { - auto info = deviceKeys.value(userId).value(deviceId); - // TODO: ed25519Verify, etc - } - } - } + // A map from SenderKey to vector of InboundSession + UnorderedMap> sessions; + void loadSessions() { - QFile file { static_cast(q->parent())->e2eeDataDir() % "/olmsessions.json" }; - if(!file.exists() || !file.open(QIODevice::ReadOnly)) { - qCDebug(E2EE) << "No 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) << "Sessions cache is empty"; - return; - } - for(const auto &senderKey : json["sessions"].toObject().keys()) { - auto pickle = json["sessions"].toObject()[senderKey].toString(); - auto sessionResult = QOlmSession::unpickle(pickle.toLatin1(), static_cast(q->parent())->picklingMode()); - if(std::holds_alternative(sessionResult)) { - qCWarning(E2EE) << "Failed to unpickle olm session"; - continue; - } - sessions[senderKey] = std::move(std::get(sessionResult)); - } + sessions = Database::instance().loadOlmSessions(static_cast(q->parent())->userId(), static_cast(q->parent())->picklingMode()); } - void saveSessions() { - QFile outFile { static_cast(q->parent())->e2eeDataDir() % "/olmsessions.json" }; - if (!outFile.open(QFile::WriteOnly)) { - qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" - << outFile.errorString(); - qCWarning(E2EE) << "Failed to write olm sessions"; + void saveSession(QOlmSessionPtr& session, const QString &senderKey) { + auto pickleResult = session->pickle(static_cast(q->parent())->picklingMode()); + if (std::holds_alternative(pickleResult)) { + qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get(pickleResult); return; } - - QJsonObject rootObj { - { QStringLiteral("cache_version"), - QJsonObject { - { QStringLiteral("major"), 1 }, - { QStringLiteral("minor"), 0 } } } - }; - { - QJsonObject sessionsJson; - for (const auto &session : sessions) { - auto pickleResult = session.second->pickle(static_cast(q->parent())->picklingMode()); - if(std::holds_alternative(pickleResult)) { - qCWarning(E2EE) << "Failed to pickle session"; - continue; - } - sessionsJson[session.first] = QString(std::get(pickleResult)); - } - rootObj.insert(QStringLiteral("sessions"), sessionsJson); - } - - const auto data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); - - outFile.write(data.data(), data.size()); - qCDebug(E2EE) << "Sessions saved to" << outFile.fileName(); + Database::instance().saveOlmSession(static_cast(q->parent())->userId(), senderKey, session->sessionId(), std::get(pickleResult)); } QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { Q_ASSERT(message.type() == QOlmMessage::PreKey); - for(auto& session : sessions) { - const auto matches = session.second->matchesInboundSessionFrom(senderKey, message); + for(auto& session : sessions[senderKey]) { + const auto matches = session->matchesInboundSessionFrom(senderKey, message); if(std::holds_alternative(matches) && std::get(matches)) { qCDebug(E2EE) << "Found inbound session"; - const auto result = session.second->decrypt(message); - saveSessions(); + const auto result = session->decrypt(message); if(std::holds_alternative(result)) { return std::get(result); } else { @@ -141,8 +80,8 @@ public: qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); } const auto result = newSession->decrypt(message); - sessions[senderKey] = std::move(newSession); - saveSessions(); + saveSession(newSession, senderKey); + sessions[senderKey].push_back(std::move(newSession)); if(std::holds_alternative(result)) { return std::get(result); } else { @@ -153,10 +92,9 @@ public: QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) { Q_ASSERT(message.type() == QOlmMessage::General); - for(auto& session : sessions) { - const auto result = session.second->decrypt(message); + for(auto& session : sessions[senderKey]) { + const auto result = session->decrypt(message); if(std::holds_alternative(result)) { - saveSessions(); return std::get(result); } } diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 2e0d7387..1b5e4441 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -3,6 +3,7 @@ #include "encryptedevent.h" #include "roommessageevent.h" +#include "events/eventloader.h" using namespace Quotient; diff --git a/lib/logging.h b/lib/logging.h index 5bf050a9..fc0a4c99 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -19,6 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB) Q_DECLARE_LOGGING_CATEGORY(NETWORK) Q_DECLARE_LOGGING_CATEGORY(PROFILER) +Q_DECLARE_LOGGING_CATEGORY(DATABASE) namespace Quotient { // QDebug manipulators 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 449456c2c86f56e9294294e03436d3aa0ff089f5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Dec 2021 09:43:00 +0100 Subject: Fix quotest invocation; use working-directory --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec6b671c..9eefa43f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get -qq install ninja-build valgrind - echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=quotest/.valgrind.supp" >>$GITHUB_ENV + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=${{ runner.workspace }}/quotest/.valgrind.supp" >>$GITHUB_ENV - name: Setup build environment run: | @@ -108,8 +108,8 @@ jobs: - name: Build and install olm if: matrix.e2ee + working-directory: '..' run: | - cd .. git clone https://gitlab.matrix.org/matrix-org/olm.git cmake -S olm -B olm/build $CMAKE_ARGS cmake --build olm/build --target install @@ -117,8 +117,8 @@ jobs: - name: Pull CS API and build GTAD if: matrix.update-api + working-directory: '..' run: | - cd .. git clone https://github.com/matrix-org/matrix-doc.git git clone --recursive https://github.com/KitsuneRal/gtad.git cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF @@ -163,10 +163,10 @@ jobs: TEST_PWD: ${{ secrets.TEST_PWD }} QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' + working-directory: build run: | - cd build ctest --output-on-failure - [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + [[ -z "$TEST_USER" ]] || $VALGRIND quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually - name: Perform CodeQL analysis -- cgit v1.2.3 From cb7c57953024a23fc7c20db75f3a15f81bb62bd3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Dec 2021 14:43:27 +0100 Subject: Fix valgrind invocation failure It turned out that, confusingly, ${{ runner.workspace }} refers to the directory above $GITHUB_WORKSPACE, which is why the previous commit ended up with valgrind not finding its suppressions. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eefa43f..6fdb337a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get -qq install ninja-build valgrind - echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=${{ runner.workspace }}/quotest/.valgrind.supp" >>$GITHUB_ENV + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV - name: Setup build environment run: | -- cgit v1.2.3 From 0128b8a296b1be7e7702ec525539614d47dd7471 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Dec 2021 14:43:39 +0100 Subject: CI: Put all build directories to ${{ runner.workspace }}/build --- .github/workflows/ci.yml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fdb337a..2e023230 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,7 @@ jobs: echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build + echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV - name: Setup MSVC environment uses: ilammy/msvc-dev-cmd@v1 @@ -108,23 +109,23 @@ jobs: - name: Build and install olm if: matrix.e2ee - working-directory: '..' + working-directory: ${{ runner.workspace }} run: | git clone https://gitlab.matrix.org/matrix-org/olm.git - cmake -S olm -B olm/build $CMAKE_ARGS - cmake --build olm/build --target install + cmake -S olm -B build/olm $CMAKE_ARGS + cmake --build build/olm --target install echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - name: Pull CS API and build GTAD if: matrix.update-api - working-directory: '..' + working-directory: ${{ runner.workspace }} run: | git clone https://github.com/matrix-org/matrix-doc.git git clone --recursive https://github.com/KitsuneRal/gtad.git - cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF - cmake --build gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ - -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ + cmake -S gtad -B build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF + cmake --build build/gtad + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc \ + -DGTAD_PATH=${{ runner.workspace }}/build/gtad/gtad" \ >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV @@ -146,15 +147,15 @@ jobs: BIN_DIR=bin fi echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV - cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} + cmake -S $GITHUB_WORKSPACE -B $BUILD_PATH $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} - name: Regenerate API code if: matrix.update-api - run: cmake --build build --target update-api + run: cmake --build ../build/libQuotient --target update-api - name: Build and install libQuotient run: | - cmake --build build --target all install + cmake --build $BUILD_PATH --target all install ls ~/.local/$BIN_DIR/quotest - name: Run tests @@ -163,9 +164,9 @@ jobs: TEST_PWD: ${{ secrets.TEST_PWD }} QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' - working-directory: build + working-directory: ../build/libQuotient run: | - ctest --output-on-failure + ctest --test-dir $BUILD_PATH --output-on-failure [[ -z "$TEST_USER" ]] || $VALGRIND quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually -- 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" --- CMakeLists.txt | 18 +- autotests/testgroupsession.cpp | 6 +- autotests/testolmaccount.cpp | 4 +- autotests/testolmsession.cpp | 2 +- autotests/testolmutility.cpp | 4 +- lib/connection.cpp | 4 +- lib/connection.h | 2 +- lib/converters.cpp | 2 +- lib/crypto/e2ee.h | 132 --------------- lib/crypto/qolmaccount.cpp | 330 ------------------------------------- lib/crypto/qolmaccount.h | 123 -------------- lib/crypto/qolmerrors.cpp | 22 --- lib/crypto/qolmerrors.h | 28 ---- lib/crypto/qolminboundsession.cpp | 153 ----------------- lib/crypto/qolminboundsession.h | 48 ------ lib/crypto/qolmmessage.cpp | 35 ---- lib/crypto/qolmmessage.h | 41 ----- lib/crypto/qolmoutboundsession.cpp | 128 -------------- lib/crypto/qolmoutboundsession.h | 54 ------ lib/crypto/qolmsession.cpp | 253 ---------------------------- lib/crypto/qolmsession.h | 76 --------- lib/crypto/qolmutility.cpp | 63 ------- lib/crypto/qolmutility.h | 45 ----- lib/crypto/qolmutils.cpp | 24 --- lib/crypto/qolmutils.h | 15 -- lib/database.cpp | 6 +- lib/database.h | 2 +- lib/e2ee/e2ee.h | 132 +++++++++++++++ lib/e2ee/qolmaccount.cpp | 330 +++++++++++++++++++++++++++++++++++++ lib/e2ee/qolmaccount.h | 123 ++++++++++++++ lib/e2ee/qolmerrors.cpp | 22 +++ lib/e2ee/qolmerrors.h | 28 ++++ lib/e2ee/qolminboundsession.cpp | 153 +++++++++++++++++ lib/e2ee/qolminboundsession.h | 48 ++++++ lib/e2ee/qolmmessage.cpp | 35 ++++ lib/e2ee/qolmmessage.h | 41 +++++ lib/e2ee/qolmoutboundsession.cpp | 128 ++++++++++++++ lib/e2ee/qolmoutboundsession.h | 54 ++++++ lib/e2ee/qolmsession.cpp | 253 ++++++++++++++++++++++++++++ lib/e2ee/qolmsession.h | 76 +++++++++ lib/e2ee/qolmutility.cpp | 63 +++++++ lib/e2ee/qolmutility.h | 45 +++++ lib/e2ee/qolmutils.cpp | 24 +++ lib/e2ee/qolmutils.h | 15 ++ lib/encryptionmanager.cpp | 12 +- lib/events/encryptedevent.h | 2 +- lib/events/encryptionevent.cpp | 2 +- lib/room.cpp | 8 +- 48 files changed, 1607 insertions(+), 1607 deletions(-) delete mode 100644 lib/crypto/e2ee.h delete mode 100644 lib/crypto/qolmaccount.cpp delete mode 100644 lib/crypto/qolmaccount.h delete mode 100644 lib/crypto/qolmerrors.cpp delete mode 100644 lib/crypto/qolmerrors.h delete mode 100644 lib/crypto/qolminboundsession.cpp delete mode 100644 lib/crypto/qolminboundsession.h delete mode 100644 lib/crypto/qolmmessage.cpp delete mode 100644 lib/crypto/qolmmessage.h delete mode 100644 lib/crypto/qolmoutboundsession.cpp delete mode 100644 lib/crypto/qolmoutboundsession.h delete mode 100644 lib/crypto/qolmsession.cpp delete mode 100644 lib/crypto/qolmsession.h delete mode 100644 lib/crypto/qolmutility.cpp delete mode 100644 lib/crypto/qolmutility.h delete mode 100644 lib/crypto/qolmutils.cpp delete mode 100644 lib/crypto/qolmutils.h create mode 100644 lib/e2ee/e2ee.h create mode 100644 lib/e2ee/qolmaccount.cpp create mode 100644 lib/e2ee/qolmaccount.h create mode 100644 lib/e2ee/qolmerrors.cpp create mode 100644 lib/e2ee/qolmerrors.h create mode 100644 lib/e2ee/qolminboundsession.cpp create mode 100644 lib/e2ee/qolminboundsession.h create mode 100644 lib/e2ee/qolmmessage.cpp create mode 100644 lib/e2ee/qolmmessage.h create mode 100644 lib/e2ee/qolmoutboundsession.cpp create mode 100644 lib/e2ee/qolmoutboundsession.h create mode 100644 lib/e2ee/qolmsession.cpp create mode 100644 lib/e2ee/qolmsession.h create mode 100644 lib/e2ee/qolmutility.cpp create mode 100644 lib/e2ee/qolmutility.h create mode 100644 lib/e2ee/qolmutils.cpp create mode 100644 lib/e2ee/qolmutils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f886094..a84a70fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,15 +164,15 @@ list(APPEND lib_SRCS ) if (${PROJECT_NAME}_ENABLE_E2EE) list(APPEND lib_SRCS - lib/crypto/qolmaccount.cpp - lib/crypto/qolmsession.cpp - lib/crypto/qolminboundsession.cpp - lib/crypto/qolmoutboundsession.cpp - lib/crypto/qolmutils.cpp - lib/crypto/qolmutility.cpp - lib/crypto/qolmerrors.cpp - lib/crypto/qolmsession.cpp - lib/crypto/qolmmessage.cpp + lib/e2ee/qolmaccount.cpp + lib/e2ee/qolmsession.cpp + lib/e2ee/qolminboundsession.cpp + lib/e2ee/qolmoutboundsession.cpp + lib/e2ee/qolmutils.cpp + lib/e2ee/qolmutility.cpp + lib/e2ee/qolmerrors.cpp + lib/e2ee/qolmsession.cpp + lib/e2ee/qolmmessage.cpp lib/encryptionmanager.cpp ) endif() diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index ea1bb4a9..afd5ef81 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -3,9 +3,9 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testgroupsession.h" -#include "crypto/qolminboundsession.h" -#include "crypto/qolmoutboundsession.h" -#include "crypto/qolmutils.h" +#include "e2ee/qolminboundsession.h" +#include "e2ee/qolmoutboundsession.h" +#include "e2ee/qolmutils.h" using namespace Quotient; diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 4fd129b5..22c457aa 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -4,8 +4,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testolmaccount.h" -#include -#include +#include +#include #include #include #include diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 00d76d4e..41baf8e3 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "crypto/qolmsession.h" +#include "e2ee/qolmsession.h" #include "testolmsession.h" using namespace Quotient; diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 2eec7e00..bbf3a055 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testolmutility.h" -#include "crypto/qolmaccount.h" -#include "crypto/qolmutility.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutility.h" using namespace Quotient; diff --git a/lib/connection.cpp b/lib/connection.cpp index f344807e..c7591e43 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -38,8 +38,8 @@ #include "jobs/syncjob.h" #ifdef Quotient_E2EE_ENABLED -# include "crypto/qolmaccount.h" -# include "crypto/qolmutils.h" +# include "e2ee/qolmaccount.h" +# include "e2ee/qolmutils.h" #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) diff --git a/lib/connection.h b/lib/connection.h index d2347d1d..3a12ec39 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -22,7 +22,7 @@ #include #ifdef Quotient_E2EE_ENABLED -#include "crypto/e2ee.h" +#include "e2ee/e2ee.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) diff --git a/lib/converters.cpp b/lib/converters.cpp index a3ac44c5..4136940f 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -4,7 +4,7 @@ #include "converters.h" #include -#include "crypto/e2ee.h" +#include "e2ee/e2ee.h" QJsonValue Quotient::JsonConverter::dump(const QVariant& v) { diff --git a/lib/crypto/e2ee.h b/lib/crypto/e2ee.h deleted file mode 100644 index 41cd2878..00000000 --- a/lib/crypto/e2ee.h +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev -// SPDX-FileCopyrightText: 2019 Kitsune Ral -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include -#include "converters.h" -#include - -#include -#include -#include -#include - -#include "util.h" - -namespace Quotient { - -inline const auto CiphertextKeyL = "ciphertext"_ls; -inline const auto SenderKeyKeyL = "sender_key"_ls; -inline const auto DeviceIdKeyL = "device_id"_ls; -inline const auto SessionIdKeyL = "session_id"_ls; - -inline const auto AlgorithmKeyL = "algorithm"_ls; -inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; -inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; - -inline const auto AlgorithmKey = QStringLiteral("algorithm"); -inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); -inline const auto RotationPeriodMsgsKey = - QStringLiteral("rotation_period_msgs"); - -inline const auto Ed25519Key = QStringLiteral("ed25519"); -inline const auto Curve25519Key = QStringLiteral("curve25519"); -inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); -inline const auto OlmV1Curve25519AesSha2AlgoKey = - QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -inline const auto MegolmV1AesSha2AlgoKey = - QStringLiteral("m.megolm.v1.aes-sha2"); -inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, - MegolmV1AesSha2AlgoKey }; -struct Unencrypted {}; -struct Encrypted { - QByteArray key; -}; - -using PicklingMode = std::variant; - -class QOlmSession; -using QOlmSessionPtr = std::unique_ptr; - -class QOlmInboundGroupSession; -using QOlmInboundGroupSessionPtr = std::unique_ptr; - -template struct overloaded : Ts... { using Ts::operator()...; }; -template overloaded(Ts...) -> overloaded; - -struct IdentityKeys -{ - QByteArray curve25519; - QByteArray ed25519; -}; - -//! Struct representing the one-time keys. -struct OneTimeKeys -{ - QMap> keys; - - //! Get the HashMap containing the curve25519 one-time keys. - QMap curve25519() const; - - //! Get a reference to the hashmap corresponding to given key type. - std::optional> get(QString keyType) const; -}; - -//! Struct representing the signed one-time keys. -class SignedOneTimeKey -{ -public: - SignedOneTimeKey() = default; - SignedOneTimeKey(const SignedOneTimeKey &) = default; - SignedOneTimeKey &operator=(const SignedOneTimeKey &) = default; - //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. - QString key; - - //! Required. Signatures of the key object. - //! The signature is calculated using the process described at Signing JSON. - QHash> signatures; -}; - - -template <> -struct JsonObjectConverter { - static void fillFrom(const QJsonObject& jo, - SignedOneTimeKey& result) - { - fromJson(jo.value("key"_ls), result.key); - fromJson(jo.value("signatures"_ls), result.signatures); - } - - static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) - { - addParam<>(jo, QStringLiteral("key"), result.key); - addParam<>(jo, QStringLiteral("signatures"), result.signatures); - } -}; - -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); - -template -class asKeyValueRange -{ -public: - asKeyValueRange(T &data) - : m_data{data} - { - } - - auto begin() { return m_data.keyValueBegin(); } - - auto end() { return m_data.keyValueEnd(); } - -private: - T &m_data; -}; - -} // namespace Quotient - -Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/crypto/qolmaccount.cpp b/lib/crypto/qolmaccount.cpp deleted file mode 100644 index 5c9f5db4..00000000 --- a/lib/crypto/qolmaccount.cpp +++ /dev/null @@ -1,330 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmaccount.h" -#include "connection.h" -#include "csapi/keys.h" -#include "crypto/qolmutils.h" -#include "crypto/qolmutility.h" -#include -#include -#include -#include - -using namespace Quotient; - -QMap OneTimeKeys::curve25519() const -{ - return keys[QStringLiteral("curve25519")]; -} - -std::optional> OneTimeKeys::get(QString keyType) const -{ - if (!keys.contains(keyType)) { - return std::nullopt; - } - return keys[keyType]; -} - -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) -{ - return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; -} - -// Convert olm error to enum -QOlmError lastError(OlmAccount *account) { - const std::string error_raw = olm_account_last_error(account); - - return fromString(error_raw); -} - -QByteArray getRandom(size_t bufferSize) -{ - QByteArray buffer(bufferSize, '0'); - std::generate(buffer.begin(), buffer.end(), std::rand); - return buffer; -} - -QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent) - : QObject(parent) - , m_userId(userId) - , m_deviceId(deviceId) -{ -} - -QOlmAccount::~QOlmAccount() -{ - olm_clear_account(m_account); - delete[](reinterpret_cast(m_account)); -} - -void QOlmAccount::createNewAccount() -{ - m_account = olm_account(new uint8_t[olm_account_size()]); - size_t randomSize = olm_create_account_random_length(m_account); - QByteArray randomData = getRandom(randomSize); - const auto error = olm_create_account(m_account, randomData.data(), randomSize); - if (error == olm_error()) { - throw lastError(m_account); - } - Q_EMIT needsSave(); -} - -void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) -{ - m_account = olm_account(new uint8_t[olm_account_size()]); - const QByteArray key = toKey(mode); - const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); - if (error == olm_error()) { - qCWarning(E2EE) << "Failed to unpickle olm account"; - //TODO: Do something that is not dying - // Probably log the user out since we have no way of getting to the keys - //throw lastError(m_account); - } -} - -std::variant QOlmAccount::pickle(const PicklingMode &mode) -{ - const QByteArray key = toKey(mode); - const size_t pickleLength = olm_pickle_account_length(m_account); - QByteArray pickleBuffer(pickleLength, '0'); - const auto error = olm_pickle_account(m_account, key.data(), - key.length(), pickleBuffer.data(), pickleLength); - if (error == olm_error()) { - return lastError(m_account); - } - return pickleBuffer; -} - -IdentityKeys QOlmAccount::identityKeys() const -{ - const size_t keyLength = olm_account_identity_keys_length(m_account); - QByteArray keyBuffer(keyLength, '0'); - const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); - if (error == olm_error()) { - throw lastError(m_account); - } - const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); - return IdentityKeys { - key.value(QStringLiteral("curve25519")).toString().toUtf8(), - key.value(QStringLiteral("ed25519")).toString().toUtf8() - }; -} - -QByteArray QOlmAccount::sign(const QByteArray &message) const -{ - QByteArray signatureBuffer(olm_account_signature_length(m_account), '0'); - - const auto error = olm_account_sign(m_account, message.data(), message.length(), - signatureBuffer.data(), signatureBuffer.length()); - - if (error == olm_error()) { - throw lastError(m_account); - } - return signatureBuffer; -} - -QByteArray QOlmAccount::sign(const QJsonObject &message) const -{ - return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); -} - -QByteArray QOlmAccount::signIdentityKeys() const -{ - const auto keys = identityKeys(); - QJsonObject body - { - {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, - {"user_id", m_userId}, - {"device_id", m_deviceId}, - {"keys", - QJsonObject{ - {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)}, - {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)} - } - } - }; - return sign(QJsonDocument(body).toJson(QJsonDocument::Compact)); - -} - -size_t QOlmAccount::maxNumberOfOneTimeKeys() const -{ - return olm_account_max_number_of_one_time_keys(m_account); -} - -size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const -{ - const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); - QByteArray randomBuffer = getRandom(randomLength); - const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); - - if (error == olm_error()) { - throw lastError(m_account); - } - Q_EMIT needsSave(); - return error; -} - -OneTimeKeys QOlmAccount::oneTimeKeys() const -{ - const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); - QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); - - const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); - if (error == olm_error()) { - throw lastError(m_account); - } - const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); - OneTimeKeys oneTimeKeys; - - for (const QString& key1 : json.keys()) { - auto oneTimeKeyObject = json[key1].toObject(); - auto keyMap = QMap(); - for (const QString &key2 : oneTimeKeyObject.keys()) { - keyMap[key2] = oneTimeKeyObject[key2].toString(); - } - oneTimeKeys.keys[key1] = keyMap; - } - return oneTimeKeys; -} - -QMap QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const -{ - QMap signedOneTimeKeys; - for (const auto &keyid : keys.curve25519().keys()) { - const auto oneTimeKey = keys.curve25519()[keyid]; - QByteArray sign = signOneTimeKey(oneTimeKey); - signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); - } - return signedOneTimeKeys; -} - -SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const -{ - SignedOneTimeKey sign{}; - sign.key = key; - sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; - return sign; -} - -QByteArray QOlmAccount::signOneTimeKey(const QString &key) const -{ - QJsonDocument j(QJsonObject{{"key", key}}); - return sign(j.toJson(QJsonDocument::Compact)); -} - -std::optional QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const -{ - const auto error = olm_remove_one_time_keys(m_account, session->raw()); - - if (error == olm_error()) { - return lastError(m_account); - } - Q_EMIT needsSave(); - return std::nullopt; -} - -OlmAccount *QOlmAccount::data() -{ - return m_account; -} - -DeviceKeys QOlmAccount::deviceKeys() const -{ - DeviceKeys deviceKeys; - deviceKeys.userId = m_userId; - deviceKeys.deviceId = m_deviceId; - deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}; - - const auto idKeys = identityKeys(); - deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519; - deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519; - - const auto sign = signIdentityKeys(); - deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; - - return deviceKeys; -} - -UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) -{ - auto keys = deviceKeys(); - - if (oneTimeKeys.curve25519().isEmpty()) { - return new UploadKeysJob(keys); - } - - // Sign & append the one time keys. - auto temp = signOneTimeKeys(oneTimeKeys); - QHash oneTimeKeysSigned; - for (const auto &[keyId, key] : asKeyValueRange(temp)) { - oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key)); - } - - return new UploadKeysJob(keys, oneTimeKeysSigned); -} - -std::variant QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) -{ - Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); - return QOlmSession::createInboundSession(this, preKeyMessage); -} - -std::variant QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) -{ - Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); - return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); -} - -std::variant QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) -{ - return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); -} - -void QOlmAccount::markKeysAsPublished() -{ - olm_account_mark_keys_as_published(m_account); - Q_EMIT needsSave(); -} - -bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, - const QString &deviceId, - const QString &userId) -{ - const auto signKeyId = "ed25519:" + deviceId; - const auto signingKey = deviceKeys.keys[signKeyId]; - const auto signature = deviceKeys.signatures[userId][signKeyId]; - - if (signature.isEmpty()) { - return false; - } - - return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); -} - -bool Quotient::ed25519VerifySignature(const QString &signingKey, - const QJsonObject &obj, - const QString &signature) -{ - if (signature.isEmpty()) { - return false; - } - QJsonObject obj1 = obj; - - obj1.remove("unsigned"); - obj1.remove("signatures"); - - auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); - - QByteArray signingKeyBuf = signingKey.toUtf8(); - QOlmUtility utility; - auto signatureBuf = signature.toUtf8(); - auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); - if (std::holds_alternative(result)) { - return false; - } - - return std::get(result); -} diff --git a/lib/crypto/qolmaccount.h b/lib/crypto/qolmaccount.h deleted file mode 100644 index dd461e8b..00000000 --- a/lib/crypto/qolmaccount.h +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - - -#pragma once - -#include "csapi/keys.h" -#include "crypto/e2ee.h" -#include "crypto/qolmerrors.h" -#include "crypto/qolmmessage.h" -#include "crypto/qolmsession.h" -#include - -struct OlmAccount; - -namespace Quotient { - -class QOlmSession; -class Connection; - -using QOlmSessionPtr = std::unique_ptr; - -//! An olm account manages all cryptographic keys used on a device. -//! \code{.cpp} -//! const auto olmAccount = new QOlmAccount(this); -//! \endcode -class QOlmAccount : public QObject -{ - Q_OBJECT -public: - QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); - ~QOlmAccount(); - - //! Creates a new instance of OlmAccount. During the instantiation - //! the Ed25519 fingerprint key pair and the Curve25519 identity key - //! pair are generated. For more information see here. - //! This needs to be called before any other action or use unpickle() instead. - void createNewAccount(); - - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. - //! This needs to be called before any other action or use createNewAccount() instead. - void unpickle(QByteArray &pickled, const PicklingMode &mode); - - //! Serialises an OlmAccount to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - - //! Returns the account's public identity keys already formatted as JSON - IdentityKeys identityKeys() const; - - //! Returns the signature of the supplied message. - QByteArray sign(const QByteArray &message) const; - QByteArray sign(const QJsonObject& message) const; - - //! Sign identity keys. - QByteArray signIdentityKeys() const; - - //! Maximum number of one time keys that this OlmAccount can - //! currently hold. - size_t maxNumberOfOneTimeKeys() const; - - //! Generates the supplied number of one time keys. - size_t generateOneTimeKeys(size_t numberOfKeys) const; - - //! Gets the OlmAccount's one time keys formatted as JSON. - OneTimeKeys oneTimeKeys() const; - - //! Sign all one time keys. - QMap signOneTimeKeys(const OneTimeKeys &keys) const; - - //! Sign one time key. - QByteArray signOneTimeKey(const QString &key) const; - - SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; - - UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); - - DeviceKeys deviceKeys() const; - - //! Remove the one time key used to create the supplied session. - [[nodiscard]] std::optional removeOneTimeKeys(const QOlmSessionPtr &session) const; - - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - //! - //! \param message An Olm pre-key message that was encrypted for this account. - std::variant createInboundSession(const QOlmMessage &preKeyMessage); - - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - //! - //! \param theirIdentityKey - The identity key of the Olm account that - //! encrypted this Olm message. - std::variant createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); - - //! Creates an outbound session for sending messages to a specific - /// identity and one time key. - std::variant createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); - - void markKeysAsPublished(); - - // HACK do not use directly - QOlmAccount(OlmAccount *account); - OlmAccount *data(); - -Q_SIGNALS: - void needsSave() const; - -private: - OlmAccount *m_account = nullptr; // owning - QString m_userId; - QString m_deviceId; -}; - -bool verifyIdentitySignature(const DeviceKeys &deviceKeys, - const QString &deviceId, - const QString &userId); - -//! checks if the signature is signed by the signing_key -bool ed25519VerifySignature(const QString &signingKey, - const QJsonObject &obj, - const QString &signature); - -} // namespace Quotient diff --git a/lib/crypto/qolmerrors.cpp b/lib/crypto/qolmerrors.cpp deleted file mode 100644 index 6db1803c..00000000 --- a/lib/crypto/qolmerrors.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - - -#include "qolmerrors.h" - -Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { - if (!error_raw.compare("BAD_ACCOUNT_KEY")) { - return QOlmError::BadAccountKey; - } else if (!error_raw.compare("BAD_MESSAGE_KEY_ID")) { - return QOlmError::BadMessageKeyId; - } else if (!error_raw.compare("INVALID_BASE64")) { - return QOlmError::InvalidBase64; - } else if (!error_raw.compare("NOT_ENOUGH_RANDOM")) { - return QOlmError::NotEnoughRandom; - } else if (!error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { - return QOlmError::OutputBufferTooSmall; - } else { - return QOlmError::Unknown; - } -} diff --git a/lib/crypto/qolmerrors.h b/lib/crypto/qolmerrors.h deleted file mode 100644 index f8390d2a..00000000 --- a/lib/crypto/qolmerrors.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -namespace Quotient { -//! All errors that could be caused by an operation regarding Olm -//! Errors are named exactly like the ones in libolm. -enum QOlmError -{ - BadAccountKey, - BadMessageFormat, - BadMessageKeyId, - BadMessageMac, - BadMessageVersion, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, - UnknownMessageIndex, - Unknown, -}; - -QOlmError fromString(const std::string &error_raw); - -} //namespace Quotient diff --git a/lib/crypto/qolminboundsession.cpp b/lib/crypto/qolminboundsession.cpp deleted file mode 100644 index 31d699f1..00000000 --- a/lib/crypto/qolminboundsession.cpp +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "crypto/qolminboundsession.h" -#include -#include -using namespace Quotient; - -QOlmError lastError(OlmInboundGroupSession *session) { - const std::string error_raw = olm_inbound_group_session_last_error(session); - - return fromString(error_raw); -} - -QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) - : m_groupSession(session) -{ -} - -QOlmInboundGroupSession::~QOlmInboundGroupSession() -{ - olm_clear_inbound_group_session(m_groupSession); - //delete[](reinterpret_cast(m_groupSession)); -} - -std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) -{ - const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - const auto temp = key; - const auto error = olm_init_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(temp.data()), temp.size()); - - if (error == olm_error()) { - throw lastError(olmInboundGroupSession); - } - - return std::make_unique(olmInboundGroupSession); -} - -std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) -{ - const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - QByteArray keyBuf = key; - - const auto error = olm_import_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(keyBuf.data()), keyBuf.size()); - if (error == olm_error()) { - throw lastError(olmInboundGroupSession); - } - - return std::make_unique(olmInboundGroupSession); -} - -QByteArray toKey(const PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return ""; - } - return std::get(mode).key; -} - -QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const -{ - QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); - const QByteArray key = toKey(mode); - const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), - pickledBuf.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } - return pickledBuf; -} - -std::variant, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.size()); - if (error == olm_error()) { - return lastError(groupSession); - } - key.clear(); - - return std::make_unique(groupSession); -} - -std::variant, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) -{ - // This is for capturing the output of olm_group_decrypt - uint32_t messageIndex = 0; - - // We need to clone the message because - // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(message.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, - reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); - - messageBuf = QByteArray(message.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), - messageBuf.length(), reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); - - // Error code or plaintext length is returned - const auto decryptError = plaintextLen; - - if (decryptError == olm_error()) { - return lastError(m_groupSession); - } - - QByteArray output(plaintextLen, '0'); - std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); - - return std::make_pair(QString(output), messageIndex); -} - -std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) -{ - const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); - QByteArray keyBuf(keyLength, '0'); - const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLength, messageIndex); - - if (error == olm_error()) { - return lastError(m_groupSession); - } - return keyBuf; -} - -uint32_t QOlmInboundGroupSession::firstKnownIndex() const -{ - return olm_inbound_group_session_first_known_index(m_groupSession); -} - -QByteArray QOlmInboundGroupSession::sessionId() const -{ - QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); - const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), - sessionIdBuf.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } - return sessionIdBuf; -} - -bool QOlmInboundGroupSession::isVerified() const -{ - return olm_inbound_group_session_is_verified(m_groupSession) != 0; -} diff --git a/lib/crypto/qolminboundsession.h b/lib/crypto/qolminboundsession.h deleted file mode 100644 index 362e42ba..00000000 --- a/lib/crypto/qolminboundsession.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include -#include -#include "olm/olm.h" -#include "crypto/qolmerrors.h" -#include "crypto/e2ee.h" - -namespace Quotient { - -//! An in-bound group session is responsible for decrypting incoming -//! communication in a Megolm session. -struct QOlmInboundGroupSession -{ -public: - ~QOlmInboundGroupSession(); - //! Creates a new instance of `OlmInboundGroupSession`. - static std::unique_ptr create(const QByteArray &key); - //! Import an inbound group session, from a previous export. - static std::unique_ptr import(const QByteArray &key); - //! Serialises an `OlmInboundGroupSession` to encrypted Base64. - QByteArray pickle(const PicklingMode &mode) const; - //! Deserialises from encrypted Base64 that was previously obtained by pickling - //! an `OlmInboundGroupSession`. - static std::variant, QOlmError> unpickle(const QByteArray &picked, const PicklingMode &mode); - //! Decrypts ciphertext received for this group session. - std::variant, QOlmError> decrypt(const QByteArray &message); - //! Export the base64-encoded ratchet key for this session, at the given index, - //! in a format which can be used by import. - std::variant exportSession(uint32_t messageIndex); - //! Get the first message index we know how to decrypt. - uint32_t firstKnownIndex() const; - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - bool isVerified() const; - QOlmInboundGroupSession(OlmInboundGroupSession *session); -private: - OlmInboundGroupSession *m_groupSession; -}; - -using QOlmInboundGroupSessionPtr = std::unique_ptr; -using OlmInboundGroupSessionPtr = std::unique_ptr; -} // namespace Quotient diff --git a/lib/crypto/qolmmessage.cpp b/lib/crypto/qolmmessage.cpp deleted file mode 100644 index 15008b75..00000000 --- a/lib/crypto/qolmmessage.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmmessage.h" - -using namespace Quotient; - -QOlmMessage::QOlmMessage(const QByteArray &ciphertext, QOlmMessage::Type type) - : QByteArray(std::move(ciphertext)) - , m_messageType(type) -{ - Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); -} - -QOlmMessage::QOlmMessage(const QOlmMessage &message) - : QByteArray(message) - , m_messageType(message.type()) -{ -} - -QOlmMessage::Type QOlmMessage::type() const -{ - return m_messageType; -} - -QByteArray QOlmMessage::toCiphertext() const -{ - return QByteArray(*this); -} - -QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) -{ - return QOlmMessage(ciphertext, QOlmMessage::General); -} diff --git a/lib/crypto/qolmmessage.h b/lib/crypto/qolmmessage.h deleted file mode 100644 index 52aba78c..00000000 --- a/lib/crypto/qolmmessage.h +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include - -namespace Quotient { - -/*! \brief A wrapper around an olm encrypted message - * - * This class encapsulates a Matrix olm encrypted message, - * passed in either of 2 forms: a general message or a pre-key message. - * - * The class provides functions to get a type and the ciphertext. - */ -class QOlmMessage : public QByteArray { - Q_GADGET -public: - enum Type { - General, - PreKey, - }; - Q_ENUM(Type) - - QOlmMessage() = default; - explicit QOlmMessage(const QByteArray &ciphertext, Type type = General); - explicit QOlmMessage(const QOlmMessage &message); - - static QOlmMessage fromCiphertext(const QByteArray &ciphertext); - - Q_INVOKABLE Type type() const; - Q_INVOKABLE QByteArray toCiphertext() const; - -private: - Type m_messageType = General; -}; - -} //namespace Quotient diff --git a/lib/crypto/qolmoutboundsession.cpp b/lib/crypto/qolmoutboundsession.cpp deleted file mode 100644 index bc572ba5..00000000 --- a/lib/crypto/qolmoutboundsession.cpp +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmoutboundsession.h" -#include "crypto/qolmutils.h" - -using namespace Quotient; - -QOlmError lastError(OlmOutboundGroupSession *session) { - const std::string error_raw = olm_outbound_group_session_last_error(session); - - return fromString(error_raw); -} - -QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) - : m_groupSession(session) -{ -} - -QOlmOutboundGroupSession::~QOlmOutboundGroupSession() -{ - olm_clear_outbound_group_session(m_groupSession); - delete[](reinterpret_cast(m_groupSession)); -} - -std::unique_ptr QOlmOutboundGroupSession::create() -{ - auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); - QByteArray randomBuf = getRandom(randomLength); - - const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - throw lastError(olmOutboundGroupSession); - } - - const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); - QByteArray keyBuffer(keyMaxLength, '0'); - olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); - - randomBuf.clear(); - - return std::make_unique(olmOutboundGroupSession); -} - -std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) -{ - QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); - QByteArray key = toKey(mode); - const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_groupSession); - } - - key.clear(); - - return pickledBuf; -} - - -std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), - pickled.data(), pickled.length()); - if (error == olm_error()) { - return lastError(olmOutboundGroupSession); - } - const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); - QByteArray idBuffer(idMaxLength, '0'); - olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - - key.clear(); - return std::make_unique(olmOutboundGroupSession); -} - -std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) -{ - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLength, '0'); - const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), - plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - return lastError(m_groupSession); - } - - return messageBuf; -} - -uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const -{ - return olm_outbound_group_session_message_index(m_groupSession); -} - -QByteArray QOlmOutboundGroupSession::sessionId() const -{ - const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); - QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } - return idBuffer; -} - -std::variant QOlmOutboundGroupSession::sessionKey() const -{ - const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); - QByteArray keyBuffer(keyMaxLength, '0'); - const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); - if (error == olm_error()) { - return lastError(m_groupSession); - } - return keyBuffer; -} diff --git a/lib/crypto/qolmoutboundsession.h b/lib/crypto/qolmoutboundsession.h deleted file mode 100644 index 4e06561e..00000000 --- a/lib/crypto/qolmoutboundsession.h +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - - -#pragma once - -#include "olm/olm.h" -#include "crypto/qolmerrors.h" -#include "crypto/e2ee.h" -#include - -namespace Quotient { - - -//! An out-bound group session is responsible for encrypting outgoing -//! communication in a Megolm session. -class QOlmOutboundGroupSession -{ -public: - ~QOlmOutboundGroupSession(); - //! Creates a new instance of `QOlmOutboundGroupSession`. - //! Throw OlmError on errors - static std::unique_ptr create(); - //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - //! Deserialises from encrypted Base64 that was previously obtained by - //! pickling a `QOlmOutboundGroupSession`. - static std::variant, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); - //! Encrypts a plaintext message using the session. - std::variant encrypt(const QString &plaintext); - - //! Get the current message index for this session. - //! - //! Each message is sent with an increasing index; this returns the - //! index for the next message. - uint32_t sessionMessageIndex() const; - - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - - //! Get the base64-encoded current ratchet key for this session. - //! - //! Each message is sent with a different ratchet key. This function returns the - //! ratchet key that will be used for the next message. - std::variant sessionKey() const; - QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); -private: - OlmOutboundGroupSession *m_groupSession; -}; - -using QOlmOutboundGroupSessionPtr = std::unique_ptr; -using OlmOutboundGroupSessionPtr = std::unique_ptr; -} diff --git a/lib/crypto/qolmsession.cpp b/lib/crypto/qolmsession.cpp deleted file mode 100644 index a0386613..00000000 --- a/lib/crypto/qolmsession.cpp +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "qolmsession.h" -#include "crypto/qolmutils.h" -#include "logging.h" -#include -#include - -using namespace Quotient; - -QOlmError lastError(OlmSession* session) { - const std::string error_raw = olm_session_last_error(session); - - return fromString(error_raw); -} - -Quotient::QOlmSession::~QOlmSession() -{ - olm_clear_session(m_session); - delete[](reinterpret_cast(m_session)); -} - -OlmSession* QOlmSession::create() -{ - return olm_session(new uint8_t[olm_session_size()]); -} - -std::variant QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) -{ - if (preKeyMessage.type() != QOlmMessage::PreKey) { - qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; - } - - const auto olmSession = create(); - - QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - size_t error = 0; - if (from) { - error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - } else { - error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - } - - if (error == olm_error()) { - const auto lastErr = lastError(olmSession); - qCWarning(E2EE) << "Error when creating inbound session" << lastErr; - return lastErr; - } - - return std::make_unique(olmSession); -} - -std::variant QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) -{ - return createInbound(account, preKeyMessage); -} - -std::variant QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) -{ - return createInbound(account, preKeyMessage, true, theirIdentityKey); -} - -std::variant QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) -{ - auto *olmOutboundSession = create(); - const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); - QByteArray randomBuf = getRandom(randomLen); - - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); - const auto error = olm_create_outbound_session(olmOutboundSession, - account->data(), - reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), - reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - const auto lastErr = lastError(olmOutboundSession); - if (lastErr == QOlmError::NotEnoughRandom) { - throw lastErr; - } - return lastErr; - } - - randomBuf.clear(); - return std::make_unique(olmOutboundSession); -} - -std::variant QOlmSession::pickle(const PicklingMode &mode) -{ - QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); - QByteArray key = toKey(mode); - const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_session); - } - - key.clear(); - - return pickledBuf; -} - -std::variant QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) -{ - QByteArray pickledBuf = pickled; - auto *olmSession = create(); - QByteArray key = toKey(mode); - const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - if (error == olm_error()) { - return lastError(olmSession); - } - - key.clear(); - return std::make_unique(olmSession); -} - -QOlmMessage QOlmSession::encrypt(const QString &plaintext) -{ - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLen, '0'); - const auto messageType = encryptMessageType(); - const auto randomLen = olm_encrypt_random_length(m_session); - QByteArray randomBuf = getRandom(randomLen); - const auto error = olm_encrypt(m_session, - reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length(), - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - throw lastError(m_session); - } - - return QOlmMessage(messageBuf, messageType); -} - -std::variant QOlmSession::decrypt(const QOlmMessage &message) const -{ - const auto messageType = message.type(); - const auto ciphertext = message.toCiphertext(); - const auto messageTypeValue = messageType == QOlmMessage::Type::General - ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; - - // We need to clone the message because - // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(ciphertext.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf.begin()); - - const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (plaintextMaxLen == olm_error()) { - return lastError(m_session); - } - - QByteArray plaintextBuf(plaintextMaxLen, '0'); - QByteArray messageBuf2(ciphertext.length(), '0'); - std::copy(message.begin(), message.end(), messageBuf2.begin()); - - const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, - reinterpret_cast(messageBuf2.data()), messageBuf2.length(), - reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); - - if (plaintextResultLen == olm_error()) { - const auto lastErr = lastError(m_session); - if (lastErr == QOlmError::OutputBufferTooSmall) { - throw lastErr; - } - return lastErr; - } - QByteArray output(plaintextResultLen, '0'); - std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); - plaintextBuf.clear(); - return output; -} - -QOlmMessage::Type QOlmSession::encryptMessageType() -{ - const auto messageTypeResult = olm_encrypt_message_type(m_session); - if (messageTypeResult == olm_error()) { - throw lastError(m_session); - } - if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { - return QOlmMessage::PreKey; - } - return QOlmMessage::General; -} - -QByteArray QOlmSession::sessionId() const -{ - const auto idMaxLength = olm_session_id_length(m_session); - QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_session); - } - return idBuffer; -} - -bool QOlmSession::hasReceivedMessage() const -{ - return olm_session_has_received_message(m_session); -} - -std::variant QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const -{ - Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); - QByteArray oneTimeKeyBuf(preKeyMessage.data()); - const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); - - if (matchesResult == olm_error()) { - return lastError(m_session); - } - switch (matchesResult) { - case 0: - return false; - case 1: - return true; - default: - return QOlmError::Unknown; - } -} -std::variant QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const -{ - const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), - oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - - if (error == olm_error()) { - return lastError(m_session); - } - switch (error) { - case 0: - return false; - case 1: - return true; - default: - return QOlmError::Unknown; - } -} - -QOlmSession::QOlmSession(OlmSession *session) - : m_session(session) -{ -} diff --git a/lib/crypto/qolmsession.h b/lib/crypto/qolmsession.h deleted file mode 100644 index 711ca66b..00000000 --- a/lib/crypto/qolmsession.h +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Alexey Andreyev -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include // FIXME: OlmSession -#include "crypto/e2ee.h" -#include "crypto/qolmmessage.h" -#include "crypto/qolmerrors.h" -#include "crypto/qolmaccount.h" - -namespace Quotient { - -class QOlmAccount; -class QOlmSession; - - -//! Either an outbound or inbound session for secure communication. -class QOlmSession -{ -public: - ~QOlmSession(); - //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::variant, QOlmError> createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage); - static std::variant, QOlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage); - static std::variant, QOlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); - //! Serialises an `QOlmSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); - //! Encrypts a plaintext message using the session. - QOlmMessage encrypt(const QString &plaintext); - - //! Decrypts a message using this session. Decoding is lossy, meaing if - //! the decrypted plaintext contains invalid UTF-8 symbols, they will - //! be returned as `U+FFFD` (�). - std::variant decrypt(const QOlmMessage &message) const; - - //! Get a base64-encoded identifier for this session. - QByteArray sessionId() const; - - //! The type of the next message that will be returned from encryption. - QOlmMessage::Type encryptMessageType(); - - //! Checker for any received messages for this session. - bool hasReceivedMessage() const; - - //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSession(const QOlmMessage &preKeyMessage) const; - - //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const; - - friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) - { - return lhs.sessionId() < rhs.sessionId(); - } - - friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { - return *lhs < *rhs; - } - - OlmSession *raw() const - { - return m_session; - } - QOlmSession(OlmSession* session); -private: - //! Helper function for creating new sessions and handling errors. - static OlmSession* create(); - static std::variant, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); - OlmSession* m_session; -}; -} //namespace Quotient diff --git a/lib/crypto/qolmutility.cpp b/lib/crypto/qolmutility.cpp deleted file mode 100644 index bb50b4d0..00000000 --- a/lib/crypto/qolmutility.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "crypto/qolmutility.h" -#include "olm/olm.h" -#include - -using namespace Quotient; - -// Convert olm error to enum -QOlmError lastError(OlmUtility *utility) { - const std::string error_raw = olm_utility_last_error(utility); - - return fromString(error_raw); -} - -QOlmUtility::QOlmUtility() -{ - auto utility = new uint8_t[olm_utility_size()]; - m_utility = olm_utility(utility); -} - -QOlmUtility::~QOlmUtility() -{ - olm_clear_utility(m_utility); - delete[](reinterpret_cast(m_utility)); -} - -QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const -{ - const auto outputLen = olm_sha256_length(m_utility); - QByteArray outputBuf(outputLen, '0'); - olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), - outputBuf.data(), outputBuf.length()); - - return QString::fromUtf8(outputBuf); -} - -QString QOlmUtility::sha256Utf8Msg(const QString &message) const -{ - return sha256Bytes(message.toUtf8()); -} - -std::variant QOlmUtility::ed25519Verify(const QByteArray &key, - const QByteArray &message, const QByteArray &signature) -{ - QByteArray signatureBuf(signature.length(), '0'); - std::copy(signature.begin(), signature.end(), signatureBuf.begin()); - - const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), - message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); - - const auto error = ret; - if (error == olm_error()) { - return lastError(m_utility); - } - - if (ret != 0) { - return false; - } - return true; -} diff --git a/lib/crypto/qolmutility.h b/lib/crypto/qolmutility.h deleted file mode 100644 index 5fd28dcc..00000000 --- a/lib/crypto/qolmutility.h +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include -#include "crypto/qolmerrors.h" - -struct OlmUtility; - -namespace Quotient { - -class QOlmSession; -class Connection; - -//! Allows you to make use of crytographic hashing via SHA-2 and -//! verifying ed25519 signatures. -class QOlmUtility -{ -public: - QOlmUtility(); - ~QOlmUtility(); - - //! Returns a sha256 of the supplied byte slice. - QString sha256Bytes(const QByteArray &inputBuf) const; - - //! Convenience function that converts the UTF-8 message - //! to bytes and then calls `sha256Bytes()`, returning its output. - QString sha256Utf8Msg(const QString &message) const; - - //! Verify a ed25519 signature. - //! \param key QByteArray The public part of the ed25519 key that signed the message. - //! \param message QByteArray The message that was signed. - //! \param signature QByteArray The signature of the message. - std::variant ed25519Verify(const QByteArray &key, - const QByteArray &message, const QByteArray &signature); - - -private: - OlmUtility *m_utility; - -}; -} diff --git a/lib/crypto/qolmutils.cpp b/lib/crypto/qolmutils.cpp deleted file mode 100644 index cd5ac83c..00000000 --- a/lib/crypto/qolmutils.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "crypto/qolmutils.h" -#include -#include - -using namespace Quotient; - -QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return {}; - } - return std::get(mode).key; -} - -QByteArray Quotient::getRandom(size_t bufferSize) -{ - QByteArray buffer(bufferSize, '0'); - RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); - return buffer; -} diff --git a/lib/crypto/qolmutils.h b/lib/crypto/qolmutils.h deleted file mode 100644 index 8b1c01ce..00000000 --- a/lib/crypto/qolmutils.h +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include - -#include "crypto/e2ee.h" - -namespace Quotient { -// Convert PicklingMode to key -QByteArray toKey(const PicklingMode &mode); -QByteArray getRandom(size_t bufferSize); -} diff --git a/lib/database.cpp b/lib/database.cpp index 153aab31..ec285d22 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -10,9 +10,9 @@ #include #include -#include "crypto/e2ee.h" -#include "crypto/qolmsession.h" -#include "crypto/qolminboundsession.h" +#include "e2ee/e2ee.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolminboundsession.h" //TODO: delete room specific data when leaving room diff --git a/lib/database.h b/lib/database.h index ed356820..8f8cd6cd 100644 --- a/lib/database.h +++ b/lib/database.h @@ -7,7 +7,7 @@ #include #include -#include "crypto/e2ee.h" +#include "e2ee/e2ee.h" namespace Quotient { class Database : public QObject diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h new file mode 100644 index 00000000..41cd2878 --- /dev/null +++ b/lib/e2ee/e2ee.h @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev +// SPDX-FileCopyrightText: 2019 Kitsune Ral +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include "converters.h" +#include + +#include +#include +#include +#include + +#include "util.h" + +namespace Quotient { + +inline const auto CiphertextKeyL = "ciphertext"_ls; +inline const auto SenderKeyKeyL = "sender_key"_ls; +inline const auto DeviceIdKeyL = "device_id"_ls; +inline const auto SessionIdKeyL = "session_id"_ls; + +inline const auto AlgorithmKeyL = "algorithm"_ls; +inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +inline const auto AlgorithmKey = QStringLiteral("algorithm"); +inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); +inline const auto RotationPeriodMsgsKey = + QStringLiteral("rotation_period_msgs"); + +inline const auto Ed25519Key = QStringLiteral("ed25519"); +inline const auto Curve25519Key = QStringLiteral("curve25519"); +inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); +inline const auto OlmV1Curve25519AesSha2AlgoKey = + QStringLiteral("m.olm.v1.curve25519-aes-sha2"); +inline const auto MegolmV1AesSha2AlgoKey = + QStringLiteral("m.megolm.v1.aes-sha2"); +inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant; + +class QOlmSession; +using QOlmSessionPtr = std::unique_ptr; + +class QOlmInboundGroupSession; +using QOlmInboundGroupSessionPtr = std::unique_ptr; + +template struct overloaded : Ts... { using Ts::operator()...; }; +template overloaded(Ts...) -> overloaded; + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the one-time keys. +struct OneTimeKeys +{ + QMap> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QMap curve25519() const; + + //! Get a reference to the hashmap corresponding to given key type. + std::optional> get(QString keyType) const; +}; + +//! Struct representing the signed one-time keys. +class SignedOneTimeKey +{ +public: + SignedOneTimeKey() = default; + SignedOneTimeKey(const SignedOneTimeKey &) = default; + SignedOneTimeKey &operator=(const SignedOneTimeKey &) = default; + //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. + QString key; + + //! Required. Signatures of the key object. + //! The signature is calculated using the process described at Signing JSON. + QHash> signatures; +}; + + +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, + SignedOneTimeKey& result) + { + fromJson(jo.value("key"_ls), result.key); + fromJson(jo.value("signatures"_ls), result.signatures); + } + + static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) + { + addParam<>(jo, QStringLiteral("key"), result.key); + addParam<>(jo, QStringLiteral("signatures"), result.signatures); + } +}; + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); + +template +class asKeyValueRange +{ +public: + asKeyValueRange(T &data) + : m_data{data} + { + } + + auto begin() { return m_data.keyValueBegin(); } + + auto end() { return m_data.keyValueEnd(); } + +private: + T &m_data; +}; + +} // namespace Quotient + +Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp new file mode 100644 index 00000000..aaf51946 --- /dev/null +++ b/lib/e2ee/qolmaccount.cpp @@ -0,0 +1,330 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmaccount.h" +#include "connection.h" +#include "csapi/keys.h" +#include "e2ee/qolmutils.h" +#include "e2ee/qolmutility.h" +#include +#include +#include +#include + +using namespace Quotient; + +QMap OneTimeKeys::curve25519() const +{ + return keys[QStringLiteral("curve25519")]; +} + +std::optional> OneTimeKeys::get(QString keyType) const +{ + if (!keys.contains(keyType)) { + return std::nullopt; + } + return keys[keyType]; +} + +bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) +{ + return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; +} + +// Convert olm error to enum +QOlmError lastError(OlmAccount *account) { + const std::string error_raw = olm_account_last_error(account); + + return fromString(error_raw); +} + +QByteArray getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + std::generate(buffer.begin(), buffer.end(), std::rand); + return buffer; +} + +QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent) + : QObject(parent) + , m_userId(userId) + , m_deviceId(deviceId) +{ +} + +QOlmAccount::~QOlmAccount() +{ + olm_clear_account(m_account); + delete[](reinterpret_cast(m_account)); +} + +void QOlmAccount::createNewAccount() +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + size_t randomSize = olm_create_account_random_length(m_account); + QByteArray randomData = getRandom(randomSize); + const auto error = olm_create_account(m_account, randomData.data(), randomSize); + if (error == olm_error()) { + throw lastError(m_account); + } + Q_EMIT needsSave(); +} + +void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + const QByteArray key = toKey(mode); + const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); + if (error == olm_error()) { + qCWarning(E2EE) << "Failed to unpickle olm account"; + //TODO: Do something that is not dying + // Probably log the user out since we have no way of getting to the keys + //throw lastError(m_account); + } +} + +std::variant QOlmAccount::pickle(const PicklingMode &mode) +{ + const QByteArray key = toKey(mode); + const size_t pickleLength = olm_pickle_account_length(m_account); + QByteArray pickleBuffer(pickleLength, '0'); + const auto error = olm_pickle_account(m_account, key.data(), + key.length(), pickleBuffer.data(), pickleLength); + if (error == olm_error()) { + return lastError(m_account); + } + return pickleBuffer; +} + +IdentityKeys QOlmAccount::identityKeys() const +{ + const size_t keyLength = olm_account_identity_keys_length(m_account); + QByteArray keyBuffer(keyLength, '0'); + const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); + if (error == olm_error()) { + throw lastError(m_account); + } + const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); + return IdentityKeys { + key.value(QStringLiteral("curve25519")).toString().toUtf8(), + key.value(QStringLiteral("ed25519")).toString().toUtf8() + }; +} + +QByteArray QOlmAccount::sign(const QByteArray &message) const +{ + QByteArray signatureBuffer(olm_account_signature_length(m_account), '0'); + + const auto error = olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureBuffer.length()); + + if (error == olm_error()) { + throw lastError(m_account); + } + return signatureBuffer; +} + +QByteArray QOlmAccount::sign(const QJsonObject &message) const +{ + return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); +} + +QByteArray QOlmAccount::signIdentityKeys() const +{ + const auto keys = identityKeys(); + QJsonObject body + { + {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, + {"user_id", m_userId}, + {"device_id", m_deviceId}, + {"keys", + QJsonObject{ + {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)}, + {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)} + } + } + }; + return sign(QJsonDocument(body).toJson(QJsonDocument::Compact)); + +} + +size_t QOlmAccount::maxNumberOfOneTimeKeys() const +{ + return olm_account_max_number_of_one_time_keys(m_account); +} + +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +{ + const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + QByteArray randomBuffer = getRandom(randomLength); + const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); + + if (error == olm_error()) { + throw lastError(m_account); + } + Q_EMIT needsSave(); + return error; +} + +OneTimeKeys QOlmAccount::oneTimeKeys() const +{ + const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); + + const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); + if (error == olm_error()) { + throw lastError(m_account); + } + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); + OneTimeKeys oneTimeKeys; + + for (const QString& key1 : json.keys()) { + auto oneTimeKeyObject = json[key1].toObject(); + auto keyMap = QMap(); + for (const QString &key2 : oneTimeKeyObject.keys()) { + keyMap[key2] = oneTimeKeyObject[key2].toString(); + } + oneTimeKeys.keys[key1] = keyMap; + } + return oneTimeKeys; +} + +QMap QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +{ + QMap signedOneTimeKeys; + for (const auto &keyid : keys.curve25519().keys()) { + const auto oneTimeKey = keys.curve25519()[keyid]; + QByteArray sign = signOneTimeKey(oneTimeKey); + signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); + } + return signedOneTimeKeys; +} + +SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const +{ + SignedOneTimeKey sign{}; + sign.key = key; + sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; + return sign; +} + +QByteArray QOlmAccount::signOneTimeKey(const QString &key) const +{ + QJsonDocument j(QJsonObject{{"key", key}}); + return sign(j.toJson(QJsonDocument::Compact)); +} + +std::optional QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const +{ + const auto error = olm_remove_one_time_keys(m_account, session->raw()); + + if (error == olm_error()) { + return lastError(m_account); + } + Q_EMIT needsSave(); + return std::nullopt; +} + +OlmAccount *QOlmAccount::data() +{ + return m_account; +} + +DeviceKeys QOlmAccount::deviceKeys() const +{ + DeviceKeys deviceKeys; + deviceKeys.userId = m_userId; + deviceKeys.deviceId = m_deviceId; + deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}; + + const auto idKeys = identityKeys(); + deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519; + deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519; + + const auto sign = signIdentityKeys(); + deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; + + return deviceKeys; +} + +UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) +{ + auto keys = deviceKeys(); + + if (oneTimeKeys.curve25519().isEmpty()) { + return new UploadKeysJob(keys); + } + + // Sign & append the one time keys. + auto temp = signOneTimeKeys(oneTimeKeys); + QHash oneTimeKeysSigned; + for (const auto &[keyId, key] : asKeyValueRange(temp)) { + oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key)); + } + + return new UploadKeysJob(keys, oneTimeKeysSigned); +} + +std::variant QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSession(this, preKeyMessage); +} + +std::variant QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); +} + +std::variant QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +{ + return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); +} + +void QOlmAccount::markKeysAsPublished() +{ + olm_account_mark_keys_as_published(m_account); + Q_EMIT needsSave(); +} + +bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, + const QString &deviceId, + const QString &userId) +{ + const auto signKeyId = "ed25519:" + deviceId; + const auto signingKey = deviceKeys.keys[signKeyId]; + const auto signature = deviceKeys.signatures[userId][signKeyId]; + + if (signature.isEmpty()) { + return false; + } + + return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); +} + +bool Quotient::ed25519VerifySignature(const QString &signingKey, + const QJsonObject &obj, + const QString &signature) +{ + if (signature.isEmpty()) { + return false; + } + QJsonObject obj1 = obj; + + obj1.remove("unsigned"); + obj1.remove("signatures"); + + auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); + + QByteArray signingKeyBuf = signingKey.toUtf8(); + QOlmUtility utility; + auto signatureBuf = signature.toUtf8(); + auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); + if (std::holds_alternative(result)) { + return false; + } + + return std::get(result); +} diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h new file mode 100644 index 00000000..00afc0e6 --- /dev/null +++ b/lib/e2ee/qolmaccount.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#pragma once + +#include "csapi/keys.h" +#include "e2ee/e2ee.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmsession.h" +#include + +struct OlmAccount; + +namespace Quotient { + +class QOlmSession; +class Connection; + +using QOlmSessionPtr = std::unique_ptr; + +//! An olm account manages all cryptographic keys used on a device. +//! \code{.cpp} +//! const auto olmAccount = new QOlmAccount(this); +//! \endcode +class QOlmAccount : public QObject +{ + Q_OBJECT +public: + QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); + ~QOlmAccount(); + + //! Creates a new instance of OlmAccount. During the instantiation + //! the Ed25519 fingerprint key pair and the Curve25519 identity key + //! pair are generated. For more information see here. + //! This needs to be called before any other action or use unpickle() instead. + void createNewAccount(); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. + //! This needs to be called before any other action or use createNewAccount() instead. + void unpickle(QByteArray &pickled, const PicklingMode &mode); + + //! Serialises an OlmAccount to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + + //! Returns the account's public identity keys already formatted as JSON + IdentityKeys identityKeys() const; + + //! Returns the signature of the supplied message. + QByteArray sign(const QByteArray &message) const; + QByteArray sign(const QJsonObject& message) const; + + //! Sign identity keys. + QByteArray signIdentityKeys() const; + + //! Maximum number of one time keys that this OlmAccount can + //! currently hold. + size_t maxNumberOfOneTimeKeys() const; + + //! Generates the supplied number of one time keys. + size_t generateOneTimeKeys(size_t numberOfKeys) const; + + //! Gets the OlmAccount's one time keys formatted as JSON. + OneTimeKeys oneTimeKeys() const; + + //! Sign all one time keys. + QMap signOneTimeKeys(const OneTimeKeys &keys) const; + + //! Sign one time key. + QByteArray signOneTimeKey(const QString &key) const; + + SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + + UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); + + DeviceKeys deviceKeys() const; + + //! Remove the one time key used to create the supplied session. + [[nodiscard]] std::optional removeOneTimeKeys(const QOlmSessionPtr &session) const; + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param message An Olm pre-key message that was encrypted for this account. + std::variant createInboundSession(const QOlmMessage &preKeyMessage); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param theirIdentityKey - The identity key of the Olm account that + //! encrypted this Olm message. + std::variant createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); + + //! Creates an outbound session for sending messages to a specific + /// identity and one time key. + std::variant createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + + void markKeysAsPublished(); + + // HACK do not use directly + QOlmAccount(OlmAccount *account); + OlmAccount *data(); + +Q_SIGNALS: + void needsSave() const; + +private: + OlmAccount *m_account = nullptr; // owning + QString m_userId; + QString m_deviceId; +}; + +bool verifyIdentitySignature(const DeviceKeys &deviceKeys, + const QString &deviceId, + const QString &userId); + +//! checks if the signature is signed by the signing_key +bool ed25519VerifySignature(const QString &signingKey, + const QJsonObject &obj, + const QString &signature); + +} // namespace Quotient diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp new file mode 100644 index 00000000..6db1803c --- /dev/null +++ b/lib/e2ee/qolmerrors.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#include "qolmerrors.h" + +Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { + if (!error_raw.compare("BAD_ACCOUNT_KEY")) { + return QOlmError::BadAccountKey; + } else if (!error_raw.compare("BAD_MESSAGE_KEY_ID")) { + return QOlmError::BadMessageKeyId; + } else if (!error_raw.compare("INVALID_BASE64")) { + return QOlmError::InvalidBase64; + } else if (!error_raw.compare("NOT_ENOUGH_RANDOM")) { + return QOlmError::NotEnoughRandom; + } else if (!error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + return QOlmError::OutputBufferTooSmall; + } else { + return QOlmError::Unknown; + } +} diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h new file mode 100644 index 00000000..f8390d2a --- /dev/null +++ b/lib/e2ee/qolmerrors.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +namespace Quotient { +//! All errors that could be caused by an operation regarding Olm +//! Errors are named exactly like the ones in libolm. +enum QOlmError +{ + BadAccountKey, + BadMessageFormat, + BadMessageKeyId, + BadMessageMac, + BadMessageVersion, + InvalidBase64, + NotEnoughRandom, + OutputBufferTooSmall, + UnknownMessageIndex, + Unknown, +}; + +QOlmError fromString(const std::string &error_raw); + +} //namespace Quotient diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp new file mode 100644 index 00000000..9bf56b6c --- /dev/null +++ b/lib/e2ee/qolminboundsession.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolminboundsession.h" +#include +#include +using namespace Quotient; + +QOlmError lastError(OlmInboundGroupSession *session) { + const std::string error_raw = olm_inbound_group_session_last_error(session); + + return fromString(error_raw); +} + +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) + : m_groupSession(session) +{ +} + +QOlmInboundGroupSession::~QOlmInboundGroupSession() +{ + olm_clear_inbound_group_session(m_groupSession); + //delete[](reinterpret_cast(m_groupSession)); +} + +std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + const auto temp = key; + const auto error = olm_init_inbound_group_session(olmInboundGroupSession, + reinterpret_cast(temp.data()), temp.size()); + + if (error == olm_error()) { + throw lastError(olmInboundGroupSession); + } + + return std::make_unique(olmInboundGroupSession); +} + +std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray keyBuf = key; + + const auto error = olm_import_inbound_group_session(olmInboundGroupSession, + reinterpret_cast(keyBuf.data()), keyBuf.size()); + if (error == olm_error()) { + throw lastError(olmInboundGroupSession); + } + + return std::make_unique(olmInboundGroupSession); +} + +QByteArray toKey(const PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return ""; + } + return std::get(mode).key; +} + +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); + const QByteArray key = toKey(mode); + const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), + pickledBuf.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return pickledBuf; +} + +std::variant, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.size()); + if (error == olm_error()) { + return lastError(groupSession); + } + key.clear(); + + return std::make_unique(groupSession); +} + +std::variant, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) +{ + // This is for capturing the output of olm_group_decrypt + uint32_t messageIndex = 0; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, + reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); + + messageBuf = QByteArray(message.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), + messageBuf.length(), reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); + + // Error code or plaintext length is returned + const auto decryptError = plaintextLen; + + if (decryptError == olm_error()) { + return lastError(m_groupSession); + } + + QByteArray output(plaintextLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); + + return std::make_pair(QString(output), messageIndex); +} + +std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +{ + const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLength, '0'); + const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLength, messageIndex); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuf; +} + +uint32_t QOlmInboundGroupSession::firstKnownIndex() const +{ + return olm_inbound_group_session_first_known_index(m_groupSession); +} + +QByteArray QOlmInboundGroupSession::sessionId() const +{ + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); + const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), + sessionIdBuf.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return sessionIdBuf; +} + +bool QOlmInboundGroupSession::isVerified() const +{ + return olm_inbound_group_session_is_verified(m_groupSession) != 0; +} diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h new file mode 100644 index 00000000..7d52991c --- /dev/null +++ b/lib/e2ee/qolminboundsession.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include "olm/olm.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/e2ee.h" + +namespace Quotient { + +//! An in-bound group session is responsible for decrypting incoming +//! communication in a Megolm session. +struct QOlmInboundGroupSession +{ +public: + ~QOlmInboundGroupSession(); + //! Creates a new instance of `OlmInboundGroupSession`. + static std::unique_ptr create(const QByteArray &key); + //! Import an inbound group session, from a previous export. + static std::unique_ptr import(const QByteArray &key); + //! Serialises an `OlmInboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by pickling + //! an `OlmInboundGroupSession`. + static std::variant, QOlmError> unpickle(const QByteArray &picked, const PicklingMode &mode); + //! Decrypts ciphertext received for this group session. + std::variant, QOlmError> decrypt(const QByteArray &message); + //! Export the base64-encoded ratchet key for this session, at the given index, + //! in a format which can be used by import. + std::variant exportSession(uint32_t messageIndex); + //! Get the first message index we know how to decrypt. + uint32_t firstKnownIndex() const; + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + bool isVerified() const; + QOlmInboundGroupSession(OlmInboundGroupSession *session); +private: + OlmInboundGroupSession *m_groupSession; +}; + +using QOlmInboundGroupSessionPtr = std::unique_ptr; +using OlmInboundGroupSessionPtr = std::unique_ptr; +} // namespace Quotient diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp new file mode 100644 index 00000000..15008b75 --- /dev/null +++ b/lib/e2ee/qolmmessage.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmmessage.h" + +using namespace Quotient; + +QOlmMessage::QOlmMessage(const QByteArray &ciphertext, QOlmMessage::Type type) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) +{ + Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); +} + +QOlmMessage::QOlmMessage(const QOlmMessage &message) + : QByteArray(message) + , m_messageType(message.type()) +{ +} + +QOlmMessage::Type QOlmMessage::type() const +{ + return m_messageType; +} + +QByteArray QOlmMessage::toCiphertext() const +{ + return QByteArray(*this); +} + +QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) +{ + return QOlmMessage(ciphertext, QOlmMessage::General); +} diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h new file mode 100644 index 00000000..52aba78c --- /dev/null +++ b/lib/e2ee/qolmmessage.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class QOlmMessage : public QByteArray { + Q_GADGET +public: + enum Type { + General, + PreKey, + }; + Q_ENUM(Type) + + QOlmMessage() = default; + explicit QOlmMessage(const QByteArray &ciphertext, Type type = General); + explicit QOlmMessage(const QOlmMessage &message); + + static QOlmMessage fromCiphertext(const QByteArray &ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type m_messageType = General; +}; + +} //namespace Quotient diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp new file mode 100644 index 00000000..88e6b2e1 --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmoutboundsession.h" +#include "e2ee/qolmutils.h" + +using namespace Quotient; + +QOlmError lastError(OlmOutboundGroupSession *session) { + const std::string error_raw = olm_outbound_group_session_last_error(session); + + return fromString(error_raw); +} + +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) + : m_groupSession(session) +{ +} + +QOlmOutboundGroupSession::~QOlmOutboundGroupSession() +{ + olm_clear_outbound_group_session(m_groupSession); + delete[](reinterpret_cast(m_groupSession)); +} + +std::unique_ptr QOlmOutboundGroupSession::create() +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); + QByteArray randomBuf = getRandom(randomLength); + + const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + throw lastError(olmOutboundGroupSession); + } + + const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); + + randomBuf.clear(); + + return std::make_unique(olmOutboundGroupSession); +} + +std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + key.clear(); + + return pickledBuf; +} + + +std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), + pickled.data(), pickled.length()); + if (error == olm_error()) { + return lastError(olmOutboundGroupSession); + } + const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); + QByteArray idBuffer(idMaxLength, '0'); + olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + + key.clear(); + return std::make_unique(olmOutboundGroupSession); +} + +std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLength, '0'); + const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), + plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + return lastError(m_groupSession); + } + + return messageBuf; +} + +uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const +{ + return olm_outbound_group_session_message_index(m_groupSession); +} + +QByteArray QOlmOutboundGroupSession::sessionId() const +{ + const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_groupSession); + } + return idBuffer; +} + +std::variant QOlmOutboundGroupSession::sessionKey() const +{ + const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); + QByteArray keyBuffer(keyMaxLength, '0'); + const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); + if (error == olm_error()) { + return lastError(m_groupSession); + } + return keyBuffer; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h new file mode 100644 index 00000000..967f563f --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#pragma once + +#include "olm/olm.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/e2ee.h" +#include + +namespace Quotient { + + +//! An out-bound group session is responsible for encrypting outgoing +//! communication in a Megolm session. +class QOlmOutboundGroupSession +{ +public: + ~QOlmOutboundGroupSession(); + //! Creates a new instance of `QOlmOutboundGroupSession`. + //! Throw OlmError on errors + static std::unique_ptr create(); + //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by + //! pickling a `QOlmOutboundGroupSession`. + static std::variant, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + std::variant encrypt(const QString &plaintext); + + //! Get the current message index for this session. + //! + //! Each message is sent with an increasing index; this returns the + //! index for the next message. + uint32_t sessionMessageIndex() const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Get the base64-encoded current ratchet key for this session. + //! + //! Each message is sent with a different ratchet key. This function returns the + //! ratchet key that will be used for the next message. + std::variant sessionKey() const; + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); +private: + OlmOutboundGroupSession *m_groupSession; +}; + +using QOlmOutboundGroupSessionPtr = std::unique_ptr; +using OlmOutboundGroupSessionPtr = std::unique_ptr; +} diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp new file mode 100644 index 00000000..69d8b431 --- /dev/null +++ b/lib/e2ee/qolmsession.cpp @@ -0,0 +1,253 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmsession.h" +#include "e2ee/qolmutils.h" +#include "logging.h" +#include +#include + +using namespace Quotient; + +QOlmError lastError(OlmSession* session) { + const std::string error_raw = olm_session_last_error(session); + + return fromString(error_raw); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); + delete[](reinterpret_cast(m_session)); +} + +OlmSession* QOlmSession::create() +{ + return olm_session(new uint8_t[olm_session_size()]); +} + +std::variant QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) +{ + if (preKeyMessage.type() != QOlmMessage::PreKey) { + qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; + } + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + const auto lastErr = lastError(olmSession); + qCWarning(E2EE) << "Error when creating inbound session" << lastErr; + return lastErr; + } + + return std::make_unique(olmSession); +} + +std::variant QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) +{ + return createInbound(account, preKeyMessage); +} + +std::variant QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +std::variant QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +{ + auto *olmOutboundSession = create(); + const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); + QByteArray randomBuf = getRandom(randomLen); + + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); + const auto error = olm_create_outbound_session(olmOutboundSession, + account->data(), + reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), + reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length()); + + if (error == olm_error()) { + const auto lastErr = lastError(olmOutboundSession); + if (lastErr == QOlmError::NotEnoughRandom) { + throw lastErr; + } + return lastErr; + } + + randomBuf.clear(); + return std::make_unique(olmOutboundSession); +} + +std::variant QOlmSession::pickle(const PicklingMode &mode) +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); + QByteArray key = toKey(mode); + const auto error = olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + + key.clear(); + + return pickledBuf; +} + +std::variant QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +{ + QByteArray pickledBuf = pickled; + auto *olmSession = create(); + QByteArray key = toKey(mode); + const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()); + if (error == olm_error()) { + return lastError(olmSession); + } + + key.clear(); + return std::make_unique(olmSession); +} + +QOlmMessage QOlmSession::encrypt(const QString &plaintext) +{ + QByteArray plaintextBuf = plaintext.toUtf8(); + const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); + QByteArray messageBuf(messageMaxLen, '0'); + const auto messageType = encryptMessageType(); + const auto randomLen = olm_encrypt_random_length(m_session); + QByteArray randomBuf = getRandom(randomLen); + const auto error = olm_encrypt(m_session, + reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length(), + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (error == olm_error()) { + throw lastError(m_session); + } + + return QOlmMessage(messageBuf, messageType); +} + +std::variant QOlmSession::decrypt(const QOlmMessage &message) const +{ + const auto messageType = message.type(); + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = messageType == QOlmMessage::Type::General + ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, + reinterpret_cast(messageBuf.data()), messageBuf.length()); + + if (plaintextMaxLen == olm_error()) { + return lastError(m_session); + } + + QByteArray plaintextBuf(plaintextMaxLen, '0'); + QByteArray messageBuf2(ciphertext.length(), '0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, + reinterpret_cast(messageBuf2.data()), messageBuf2.length(), + reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); + + if (plaintextResultLen == olm_error()) { + const auto lastErr = lastError(m_session); + if (lastErr == QOlmError::OutputBufferTooSmall) { + throw lastErr; + } + return lastErr; + } + QByteArray output(plaintextResultLen, '0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); + plaintextBuf.clear(); + return output; +} + +QOlmMessage::Type QOlmSession::encryptMessageType() +{ + const auto messageTypeResult = olm_encrypt_message_type(m_session); + if (messageTypeResult == olm_error()) { + throw lastError(m_session); + } + if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { + return QOlmMessage::PreKey; + } + return QOlmMessage::General; +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '0'); + const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), + idBuffer.length()); + if (error == olm_error()) { + throw lastError(m_session); + } + return idBuffer; +} + +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + +std::variant QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + + if (matchesResult == olm_error()) { + return lastError(m_session); + } + switch (matchesResult) { + case 0: + return false; + case 1: + return true; + default: + return QOlmError::Unknown; + } +} +std::variant QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const +{ + const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + + if (error == olm_error()) { + return lastError(m_session); + } + switch (error) { + case 0: + return false; + case 1: + return true; + default: + return QOlmError::Unknown; + } +} + +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) +{ +} diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h new file mode 100644 index 00000000..1febfa0f --- /dev/null +++ b/lib/e2ee/qolmsession.h @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include // FIXME: OlmSession +#include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolmaccount.h" + +namespace Quotient { + +class QOlmAccount; +class QOlmSession; + + +//! Either an outbound or inbound session for secure communication. +class QOlmSession +{ +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static std::variant, QOlmError> createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage); + static std::variant, QOlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage); + static std::variant, QOlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); + //! Serialises an `QOlmSession` to encrypted Base64. + std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. + static std::variant, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); + //! Encrypts a plaintext message using the session. + QOlmMessage encrypt(const QString &plaintext); + + //! Decrypts a message using this session. Decoding is lossy, meaing if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + std::variant decrypt(const QOlmMessage &message) const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! The type of the next message that will be returned from encryption. + QOlmMessage::Type encryptMessageType(); + + //! Checker for any received messages for this session. + bool hasReceivedMessage() const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant matchesInboundSession(const QOlmMessage &preKeyMessage) const; + + //! Checks if the 'prekey' message is for this in-bound session. + std::variant matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const; + + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { + return *lhs < *rhs; + } + + OlmSession *raw() const + { + return m_session; + } + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static std::variant, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; +} //namespace Quotient diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp new file mode 100644 index 00000000..d0684055 --- /dev/null +++ b/lib/e2ee/qolmutility.cpp @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutility.h" +#include "olm/olm.h" +#include + +using namespace Quotient; + +// Convert olm error to enum +QOlmError lastError(OlmUtility *utility) { + const std::string error_raw = olm_utility_last_error(utility); + + return fromString(error_raw); +} + +QOlmUtility::QOlmUtility() +{ + auto utility = new uint8_t[olm_utility_size()]; + m_utility = olm_utility(utility); +} + +QOlmUtility::~QOlmUtility() +{ + olm_clear_utility(m_utility); + delete[](reinterpret_cast(m_utility)); +} + +QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const +{ + const auto outputLen = olm_sha256_length(m_utility); + QByteArray outputBuf(outputLen, '0'); + olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), + outputBuf.data(), outputBuf.length()); + + return QString::fromUtf8(outputBuf); +} + +QString QOlmUtility::sha256Utf8Msg(const QString &message) const +{ + return sha256Bytes(message.toUtf8()); +} + +std::variant QOlmUtility::ed25519Verify(const QByteArray &key, + const QByteArray &message, const QByteArray &signature) +{ + QByteArray signatureBuf(signature.length(), '0'); + std::copy(signature.begin(), signature.end(), signatureBuf.begin()); + + const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), + message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); + + const auto error = ret; + if (error == olm_error()) { + return lastError(m_utility); + } + + if (ret != 0) { + return false; + } + return true; +} diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h new file mode 100644 index 00000000..b360d625 --- /dev/null +++ b/lib/e2ee/qolmutility.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include "e2ee/qolmerrors.h" + +struct OlmUtility; + +namespace Quotient { + +class QOlmSession; +class Connection; + +//! Allows you to make use of crytographic hashing via SHA-2 and +//! verifying ed25519 signatures. +class QOlmUtility +{ +public: + QOlmUtility(); + ~QOlmUtility(); + + //! Returns a sha256 of the supplied byte slice. + QString sha256Bytes(const QByteArray &inputBuf) const; + + //! Convenience function that converts the UTF-8 message + //! to bytes and then calls `sha256Bytes()`, returning its output. + QString sha256Utf8Msg(const QString &message) const; + + //! Verify a ed25519 signature. + //! \param key QByteArray The public part of the ed25519 key that signed the message. + //! \param message QByteArray The message that was signed. + //! \param signature QByteArray The signature of the message. + std::variant ed25519Verify(const QByteArray &key, + const QByteArray &message, const QByteArray &signature); + + +private: + OlmUtility *m_utility; + +}; +} diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp new file mode 100644 index 00000000..ce27710d --- /dev/null +++ b/lib/e2ee/qolmutils.cpp @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutils.h" +#include +#include + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative(mode)) { + return {}; + } + return std::get(mode).key; +} + +QByteArray Quotient::getRandom(size_t bufferSize) +{ + QByteArray buffer(bufferSize, '0'); + RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); + return buffer; +} diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h new file mode 100644 index 00000000..bbd71332 --- /dev/null +++ b/lib/e2ee/qolmutils.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +#include "e2ee/e2ee.h" + +namespace Quotient { +// Convert PicklingMode to key +QByteArray toKey(const PicklingMode &mode); +QByteArray getRandom(size_t bufferSize); +} diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index e5fa978f..3d616965 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -6,7 +6,6 @@ #include "encryptionmanager.h" #include "connection.h" -#include "crypto/e2ee.h" #include "events/encryptedfile.h" #include "database.h" @@ -16,11 +15,12 @@ #include #include -#include "crypto/qolmaccount.h" -#include "crypto/qolmsession.h" -#include "crypto/qolmmessage.h" -#include "crypto/qolmerrors.h" -#include "crypto/qolmutils.h" +#include "e2ee/e2ee.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmerrors.h" +#include "e2ee/qolmutils.h" #include #include diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 28398827..4cc3bf8e 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -3,7 +3,7 @@ #pragma once -#include "crypto/e2ee.h" +#include "e2ee/e2ee.h" #include "roomevent.h" namespace Quotient { diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index d7bb953a..6272c668 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -4,7 +4,7 @@ #include "encryptionevent.h" -#include "crypto/e2ee.h" +#include "e2ee/e2ee.h" #include 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 0f20fb028602016c718f4fd05cdb18b80442b3ca Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 19:09:14 +0100 Subject: id -> matrixId --- lib/database.cpp | 16 ++++++++-------- lib/database.h | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index ec285d22..e115b6c3 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -89,11 +89,11 @@ void Database::migrateTo1() commit(); } -QByteArray Database::accountPickle(const QString &id) +QByteArray Database::accountPickle(const QString &matrixId) { QSqlQuery query; query.prepare(QStringLiteral("SELECT pickle FROM Accounts WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", id); + query.bindValue(":matrixId", matrixId); execute(query); if (query.next()) { return query.value(QStringLiteral("pickle")).toByteArray(); @@ -101,30 +101,30 @@ QByteArray Database::accountPickle(const QString &id) return {}; } -void Database::setAccountPickle(const QString &id, const QByteArray &pickle) +void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickle) { QSqlQuery query; query.prepare(QStringLiteral("INSERT INTO Accounts(matrixId, pickle) VALUES(:matrixId, :pickle) ON CONFLICT (matrixId) DO UPDATE SET pickle=:pickle WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", id); + query.bindValue(":matrixId", matrixId); query.bindValue(":pickle", pickle); transaction(); execute(query); commit(); } -void Database::clear(const QString &id) +void Database::clear(const QString &matrixId) { QSqlQuery query; query.prepare(QStringLiteral("DELETE FROM Accounts(matrixId, pickle) WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", id); + query.bindValue(":matrixId", matrixId); QSqlQuery sessionsQuery; sessionsQuery.prepare(QStringLiteral("DELETE FROM OlmSessions WHERE matrixId=:matrixId;")); - sessionsQuery.bindValue(":matrixId", id); + sessionsQuery.bindValue(":matrixId", matrixId); QSqlQuery megolmSessionsQuery; megolmSessionsQuery.prepare(QStringLiteral("DELETE FROM InboundMegolmSessions WHERE matrixId=:matrixId;")); - megolmSessionsQuery.bindValue(":matrixId", id); + megolmSessionsQuery.bindValue(":matrixId", matrixId); QSqlQuery groupSessionIndexRecordQuery; groupSessionIndexRecordQuery.prepare(QStringLiteral("DELETE FROM GroupSessionIndexRecord WHERE matrixId=:matrixId;")); diff --git a/lib/database.h b/lib/database.h index 8f8cd6cd..25af2833 100644 --- a/lib/database.h +++ b/lib/database.h @@ -27,9 +27,9 @@ public: QSqlQuery execute(const QString &queryString); QSqlQuery execute(QSqlQuery &query); - QByteArray accountPickle(const QString &id); - void setAccountPickle(const QString &id, const QByteArray &pickle); - void clear(const QString &id); + QByteArray accountPickle(const QString &matrixId); + void setAccountPickle(const QString &matrixId, const QByteArray &pickle); + void clear(const QString &matrixId); void saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle); UnorderedMap> loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode); UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode); -- 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/database.cpp | 2 +- lib/room.cpp | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index e115b6c3..6acfbc74 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -115,7 +115,7 @@ void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickl void Database::clear(const QString &matrixId) { QSqlQuery query; - query.prepare(QStringLiteral("DELETE FROM Accounts(matrixId, pickle) WHERE matrixId=:matrixId;")); + query.prepare(QStringLiteral("DELETE FROM Accounts WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); QSqlQuery sessionsQuery; 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 1cf9b67e3586888e5f72a30b82bb9541c026d672 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 22:05:12 +0100 Subject: snake_case table names --- lib/database.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 6acfbc74..5372ad7e 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -80,11 +80,11 @@ void Database::migrateTo1() { qDebug() << "Migrating database to version 1"; transaction(); - execute(QStringLiteral("CREATE TABLE Accounts (matrixId TEXT UNIQUE, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE OlmSessions (matrixId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE InboundMegolmSessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE accounts (matrixId TEXT UNIQUE, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE olm_sessions (matrixId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE OutboundMegolmSessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE GroupSessionIndexRecord (matrixId TEXT, roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("CREATE TABLE group_session_record_index (matrixId TEXT, roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); } @@ -92,7 +92,7 @@ void Database::migrateTo1() QByteArray Database::accountPickle(const QString &matrixId) { QSqlQuery query; - query.prepare(QStringLiteral("SELECT pickle FROM Accounts WHERE matrixId=:matrixId;")); + query.prepare(QStringLiteral("SELECT pickle FROM accounts WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); execute(query); if (query.next()) { @@ -104,7 +104,7 @@ QByteArray Database::accountPickle(const QString &matrixId) void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickle) { QSqlQuery query; - query.prepare(QStringLiteral("INSERT INTO Accounts(matrixId, pickle) VALUES(:matrixId, :pickle) ON CONFLICT (matrixId) DO UPDATE SET pickle=:pickle WHERE matrixId=:matrixId;")); + query.prepare(QStringLiteral("INSERT INTO accounts(matrixId, pickle) VALUES(:matrixId, :pickle) ON CONFLICT (matrixId) DO UPDATE SET pickle=:pickle WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); query.bindValue(":pickle", pickle); transaction(); @@ -115,19 +115,19 @@ void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickl void Database::clear(const QString &matrixId) { QSqlQuery query; - query.prepare(QStringLiteral("DELETE FROM Accounts WHERE matrixId=:matrixId;")); + query.prepare(QStringLiteral("DELETE FROM accounts WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); QSqlQuery sessionsQuery; - sessionsQuery.prepare(QStringLiteral("DELETE FROM OlmSessions WHERE matrixId=:matrixId;")); + sessionsQuery.prepare(QStringLiteral("DELETE FROM olm_sessions WHERE matrixId=:matrixId;")); sessionsQuery.bindValue(":matrixId", matrixId); QSqlQuery megolmSessionsQuery; - megolmSessionsQuery.prepare(QStringLiteral("DELETE FROM InboundMegolmSessions WHERE matrixId=:matrixId;")); + megolmSessionsQuery.prepare(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE matrixId=:matrixId;")); megolmSessionsQuery.bindValue(":matrixId", matrixId); QSqlQuery groupSessionIndexRecordQuery; - groupSessionIndexRecordQuery.prepare(QStringLiteral("DELETE FROM GroupSessionIndexRecord WHERE matrixId=:matrixId;")); + groupSessionIndexRecordQuery.prepare(QStringLiteral("DELETE FROM group_session_record_index WHERE matrixId=:matrixId;")); groupSessionIndexRecordQuery.bindValue(":matrixId", matrixId); transaction(); @@ -142,7 +142,7 @@ void Database::clear(const QString &matrixId) void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle) { QSqlQuery query; - query.prepare(QStringLiteral("INSERT INTO OlmSessions(matrixId, senderKey, sessionId, pickle) VALUES(:matrixId, :senderKey, :sessionId, :pickle);")); + query.prepare(QStringLiteral("INSERT INTO olm_sessions(matrixId, senderKey, sessionId, pickle) VALUES(:matrixId, :senderKey, :sessionId, :pickle);")); query.bindValue(":matrixId", matrixId); query.bindValue(":senderKey", senderKey); query.bindValue(":sessionId", sessionId); @@ -155,7 +155,7 @@ void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, UnorderedMap> Database::loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode) { QSqlQuery query; - query.prepare(QStringLiteral("SELECT * FROM OlmSessions WHERE matrixId=:matrixId;")); + query.prepare(QStringLiteral("SELECT * FROM olm_sessions WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); transaction(); execute(query); @@ -175,7 +175,7 @@ UnorderedMap> Database::loadOlmSessions(con UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode) { QSqlQuery query; - query.prepare(QStringLiteral("SELECT * FROM InboundMegolmSessions WHERE matrixId=:matrixId AND roomId=:roomId;")); + query.prepare(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE matrixId=:matrixId AND roomId=:roomId;")); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); transaction(); @@ -196,7 +196,7 @@ UnorderedMap, QOlmInboundGroupSessionPtr> Database::load void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) { QSqlQuery query; - query.prepare(QStringLiteral("INSERT INTO InboundMegolmSessions(matrixId, roomId, senderKey, sessionId, pickle) VALUES(:matrixId, :roomId, :senderKey, :sessionId, :pickle);")); + query.prepare(QStringLiteral("INSERT INTO inbound_megolm_sessions(matrixId, roomId, senderKey, sessionId, pickle) VALUES(:matrixId, :roomId, :senderKey, :sessionId, :pickle);")); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); query.bindValue(":senderKey", senderKey); @@ -210,7 +210,7 @@ void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) { QSqlQuery query; - query.prepare("INSERT INTO GroupSessionIndexRecord(matrixId, roomId, sessionId, i, eventId, ts) VALUES(:matrixId, :roomId, :sessionId, :index, :eventId, :ts);"); + query.prepare("INSERT INTO group_session_record_index(matrixId, roomId, sessionId, i, eventId, ts) VALUES(:matrixId, :roomId, :sessionId, :index, :eventId, :ts);"); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); @@ -225,7 +225,7 @@ void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString QPair Database::groupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, qint64 index) { QSqlQuery query; - query.prepare(QStringLiteral("SELECT * FROM GroupSessionIndexRecord WHERE matrixId=:matrixId AND roomId=:roomId AND sessionId=:sessionId AND i=:index;")); + query.prepare(QStringLiteral("SELECT * FROM group_session_record_index WHERE matrixId=:matrixId AND roomId=:roomId AND sessionId=:sessionId AND i=:index;")); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); -- cgit v1.2.3 From 18695131e056b86e38d7b43f787014fbd1516240 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 22:10:42 +0100 Subject: Remove default constructor --- lib/encryptionmanager.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 3d616965..c0e44f70 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -31,11 +31,6 @@ using std::move; class EncryptionManager::Private { public: - explicit Private() - { - } - ~Private() = default; - EncryptionManager* q; // A map from SenderKey to vector of InboundSession -- cgit v1.2.3 From 6cf625779fe270fd4192639e30acc45687270246 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 7 Dec 2021 23:07:57 +0100 Subject: Add clangd files to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 769bdf45..d414f49f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ Makefile Quotient_autogen/ .cmake/ tests/.cmake/ + +# clangd +.cache/ +compile_commands.json -- 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(-) 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 bd728a91f69382a052d07582788fcecec8b0f35e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Dec 2021 19:00:04 +0100 Subject: Test installed quotest This covers Quotient_INSTALL_TESTS setting. --- .github/workflows/ci.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e023230..9d286da6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,8 +98,16 @@ jobs: echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV + + if [[ '${{ runner.os }}' != 'Windows' ]]; then + BIN_DIR=/bin + fi + echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV + echo "~/.local$BIN_DIR" >>$GITHUB_PATH + cmake -E make_directory ${{ runner.workspace }}/build echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV + echo "LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH" >>$GITHUB_ENV - name: Setup MSVC environment uses: ilammy/msvc-dev-cmd@v1 @@ -141,13 +149,8 @@ jobs: - name: Configure libQuotient run: | - if [[ '${{ runner.os }}' == 'Windows' ]]; then - BIN_DIR=. - else - BIN_DIR=bin - fi - echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV - cmake -S $GITHUB_WORKSPACE -B $BUILD_PATH $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} + cmake -S $GITHUB_WORKSPACE -B $BUILD_PATH $CMAKE_ARGS \ + -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} -DQuotient_INSTALL_TESTS=ON - name: Regenerate API code if: matrix.update-api @@ -164,10 +167,10 @@ jobs: TEST_PWD: ${{ secrets.TEST_PWD }} QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' - working-directory: ../build/libQuotient run: | ctest --test-dir $BUILD_PATH --output-on-failure - [[ -z "$TEST_USER" ]] || $VALGRIND quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + [[ -z "$TEST_USER" ]] || \ + $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually - name: Perform CodeQL analysis -- 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/events/roomevent.cpp | 13 +++++++++++++ lib/events/roomevent.h | 4 ++++ lib/room.cpp | 7 +++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index fb921af6..b99d1381 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -126,3 +126,16 @@ CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) if (callId().isEmpty()) qCWarning(EVENTS) << id() << "is a call event with an empty call id"; } + +void RoomEvent::setOriginalEvent(event_ptr_tt originalEvent) +{ + _originalEvent = std::move(originalEvent); +} + +const QJsonObject RoomEvent::encryptedJson() const +{ + if(!_originalEvent) { + return {}; + } + return _originalEvent->fullJson(); +} diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 7f13f6f2..35527a62 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -60,11 +60,15 @@ public: //! callback for that in RoomEvent. void addId(const QString& newId); + void setOriginalEvent(event_ptr_tt originalEvent); + const QJsonObject encryptedJson() const; + protected: void dumpTo(QDebug dbg) const override; private: event_ptr_tt _redactedBecause; + event_ptr_tt _originalEvent; }; using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; 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 3054255cba206c91e3bdf0ea42fde39d51261e6a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 8 Dec 2021 23:14:29 +0100 Subject: Update logging categories --- lib/database.cpp | 4 ++-- lib/logging.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 5372ad7e..9bdcd9e6 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -36,7 +36,7 @@ int Database::version() if (query.next()) { bool ok; int value = query.value(0).toInt(&ok); - qDebug() << "Database version" << value; + qCDebug(DATABASE) << "Database version" << value; if (ok) return value; } else { @@ -78,7 +78,7 @@ void Database::commit() void Database::migrateTo1() { - qDebug() << "Migrating database to version 1"; + qCDebug(DATABASE) << "Migrating database to version 1"; transaction(); execute(QStringLiteral("CREATE TABLE accounts (matrixId TEXT UNIQUE, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (matrixId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); diff --git a/lib/logging.cpp b/lib/logging.cpp index 15eac69d..460caced 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -19,3 +19,4 @@ LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") LOGGING_CATEGORY(NETWORK, "quotient.network") LOGGING_CATEGORY(PROFILER, "quotient.profiler") +LOGGING_CATEGORY(DATABASE, "quotient.database") -- cgit v1.2.3 From 39ca00755d91c5e608bfe2a67c84ceb29ed49976 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 8 Dec 2021 23:18:27 +0100 Subject: Try fixing sonar --- .github/workflows/sonar.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 76db59c9..c987b0cc 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -64,6 +64,14 @@ jobs: cmake -S olm -B olm/build $CMAKE_ARGS cmake --build olm/build --target install + - name: Build and install qtKeychain + if: matrix.e2ee + run: | + cd .. + git clone https://github.com/frankosterfeld/qtkeychain.git + cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS + cmake --build qtkeychain/build --target install + - name: Pull CS API and build GTAD if: matrix.update-api run: | -- cgit v1.2.3 From c585227b3724666e3cf7aab3ab53d6f5930e7218 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Dec 2021 21:07:52 +0100 Subject: Fix CI failure on macOS CMAKE_INSTALL_RPATH_USE_LINK_PATH is more universal than setting LD_LIBRARY_PATH Also: drop an extra slash in the path to installed quotest. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d286da6..c78a5981 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,7 +97,8 @@ jobs: fi echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ - -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV + -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local\ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" >>$GITHUB_ENV if [[ '${{ runner.os }}' != 'Windows' ]]; then BIN_DIR=/bin @@ -107,7 +108,6 @@ jobs: cmake -E make_directory ${{ runner.workspace }}/build echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV - echo "LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH" >>$GITHUB_ENV - name: Setup MSVC environment uses: ilammy/msvc-dev-cmd@v1 @@ -159,7 +159,7 @@ jobs: - name: Build and install libQuotient run: | cmake --build $BUILD_PATH --target all install - ls ~/.local/$BIN_DIR/quotest + ls ~/.local$BIN_DIR/quotest - name: Run tests env: -- cgit v1.2.3 From 1f6771c3b14453ae9b6651a9edb1f7778d3f71f3 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 9 Dec 2021 23:59:38 +0100 Subject: Update lib/connection.cpp Co-authored-by: Alexey Rusakov --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index c7591e43..b7aaca86 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2041,7 +2041,7 @@ PicklingMode Connection::picklingMode() const void Connection::saveOlmAccount() { - qDebug() << "Saving olm account"; + qCDebug(E2EE) << "Saving olm account"; #ifdef Quotient_E2EE_ENABLED auto pickle = d->olmAccount->pickle(d->picklingMode); Database::instance().setAccountPickle(userId(), std::get(pickle)); -- 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 --- CMakeLists.txt | 13 ++++++++----- lib/connection.cpp | 12 ++++-------- lib/events/roomevent.cpp | 2 ++ lib/events/roomevent.h | 5 +++++ lib/room.cpp | 6 +++++- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a84a70fb..43fed3e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,11 +83,12 @@ else() set(QtExtraModules "Multimedia") # See #483 endif() string(REGEX REPLACE "^(.).*" "Qt\\1" Qt ${QtMinVersion}) # makes "Qt5" or "Qt6" -find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test Sql ${QtExtraModules}) +find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModules}) get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") if (${PROJECT_NAME}_ENABLE_E2EE) + find_package(${Qt} ${QtMinVersion} REQUIRED Sql) find_package(Olm 3.2.1 REQUIRED) set_package_properties(Olm PROPERTIES DESCRIPTION "Implementation of the Olm and Megolm cryptographic ratchets" @@ -107,9 +108,9 @@ if (${PROJECT_NAME}_ENABLE_E2EE) if (OpenSSL_FOUND) message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}") endif() + find_package(${Qt}Keychain REQUIRED) endif() -find_package(${Qt}Keychain REQUIRED) # Set up source files list(APPEND lib_SRCS @@ -133,7 +134,6 @@ list(APPEND lib_SRCS lib/eventitem.cpp lib/accountregistry.cpp lib/mxcreply.cpp - lib/database.cpp lib/events/event.cpp lib/events/roomevent.cpp lib/events/stateevent.cpp @@ -164,6 +164,7 @@ list(APPEND lib_SRCS ) if (${PROJECT_NAME}_ENABLE_E2EE) list(APPEND lib_SRCS + lib/database.cpp lib/e2ee/qolmaccount.cpp lib/e2ee/qolmsession.cpp lib/e2ee/qolminboundsession.cpp @@ -323,12 +324,14 @@ target_include_directories(${PROJECT_NAME} PUBLIC if (${PROJECT_NAME}_ENABLE_E2EE) target_link_libraries(${PROJECT_NAME} Olm::Olm OpenSSL::Crypto - OpenSSL::SSL) + OpenSSL::SSL + ${Qt}::Sql + ${QTKEYCHAIN_LIBRARIES}) set(FIND_DEPS "find_dependency(Olm) find_dependency(OpenSSL)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${Qt}::Sql ${QTKEYCHAIN_LIBRARIES}) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) diff --git a/lib/connection.cpp b/lib/connection.cpp index b7aaca86..433dd942 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -7,9 +7,6 @@ #include "connection.h" #include "connectiondata.h" -#ifdef Quotient_E2EE_ENABLED -# include "encryptionmanager.h" -#endif // Quotient_E2EE_ENABLED #include "room.h" #include "settings.h" #include "user.h" @@ -40,6 +37,8 @@ #ifdef Quotient_E2EE_ENABLED # include "e2ee/qolmaccount.h" # include "e2ee/qolmutils.h" +# include "encryptionmanager.h" +# include "database.h" #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -62,7 +61,6 @@ # include #endif -#include "database.h" using namespace Quotient; @@ -274,9 +272,9 @@ Connection::Connection(const QUrl& server, QObject* parent) connect(qApp, &QCoreApplication::aboutToQuit, this, [this](){ saveOlmAccount(); }); + Database::instance(); #endif d->q = this; // All d initialization should occur before this line - Database::instance(); } Connection::Connection(QObject* parent) : Connection({}, parent) {} @@ -442,15 +440,13 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) auto loginJob = q->callApi(std::forward(loginArgs)...); connect(loginJob, &BaseJob::success, q, [this, loginJob] { - Database::instance().clear(loginJob->userId()); data->setToken(loginJob->accessToken().toLatin1()); data->setDeviceId(loginJob->deviceId()); completeSetup(loginJob->userId()); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED - //encryptionManager->uploadIdentityKeys(q); - //encryptionManager->uploadOneTimeKeys(q); + Database::instance().clear(loginJob->userId()); #endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index b99d1381..dbce2255 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -127,6 +127,7 @@ CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) qCWarning(EVENTS) << id() << "is a call event with an empty call id"; } +#ifdef Quotient_E2EE_ENABLED void RoomEvent::setOriginalEvent(event_ptr_tt originalEvent) { _originalEvent = std::move(originalEvent); @@ -139,3 +140,4 @@ const QJsonObject RoomEvent::encryptedJson() const } return _originalEvent->fullJson(); } +#endif diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 35527a62..36b45f09 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -60,15 +60,20 @@ public: //! callback for that in RoomEvent. void addId(const QString& newId); +#ifdef Quotient_E2EE_ENABLED void setOriginalEvent(event_ptr_tt originalEvent); const QJsonObject encryptedJson() const; +#endif protected: void dumpTo(QDebug dbg) const override; private: event_ptr_tt _redactedBecause; + +#ifdef Quotient_E2EE_ENABLED event_ptr_tt _originalEvent; +#endif }; using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; 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 b3be614b71b12e729d1bf3d6ca7d7068a0786fc8 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 9 Dec 2021 23:56:58 +0100 Subject: Rename database --- lib/database.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 9bdcd9e6..01015d3c 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -19,11 +19,11 @@ using namespace Quotient; Database::Database() { - QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient")); QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir(databasePath).mkpath(databasePath); - QSqlDatabase::database().setDatabaseName(databasePath + QStringLiteral("/database.db3")); - QSqlDatabase::database().open(); + QSqlDatabase::database(QStringLiteral("Quotient")).setDatabaseName(databasePath + QStringLiteral("/quotient.db3")); + QSqlDatabase::database(QStringLiteral("Quotient")).open(); switch(version()) { case 0: migrateTo1(); @@ -47,7 +47,7 @@ int Database::version() QSqlQuery Database::execute(const QString &queryString) { - auto query = QSqlDatabase::database().exec(queryString); + auto query = QSqlDatabase::database(QStringLiteral("Quotient")).exec(queryString); if (query.lastError().type() != QSqlError::NoError) { qCritical() << "Failed to execute query"; qCritical() << query.lastQuery(); @@ -68,12 +68,12 @@ QSqlQuery Database::execute(QSqlQuery &query) void Database::transaction() { - QSqlDatabase::database().transaction(); + QSqlDatabase::database(QStringLiteral("Quotient")).transaction(); } void Database::commit() { - QSqlDatabase::database().commit(); + QSqlDatabase::database(QStringLiteral("Quotient")).commit(); } void Database::migrateTo1() @@ -83,7 +83,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (matrixId TEXT UNIQUE, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (matrixId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE OutboundMegolmSessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (matrixId TEXT, roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -91,7 +91,7 @@ void Database::migrateTo1() QByteArray Database::accountPickle(const QString &matrixId) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("SELECT pickle FROM accounts WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); execute(query); @@ -103,7 +103,7 @@ QByteArray Database::accountPickle(const QString &matrixId) void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickle) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("INSERT INTO accounts(matrixId, pickle) VALUES(:matrixId, :pickle) ON CONFLICT (matrixId) DO UPDATE SET pickle=:pickle WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); query.bindValue(":pickle", pickle); @@ -114,19 +114,19 @@ void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickl void Database::clear(const QString &matrixId) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("DELETE FROM accounts WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); - QSqlQuery sessionsQuery; + QSqlQuery sessionsQuery(QSqlDatabase::database(QStringLiteral("Quotient"))); sessionsQuery.prepare(QStringLiteral("DELETE FROM olm_sessions WHERE matrixId=:matrixId;")); sessionsQuery.bindValue(":matrixId", matrixId); - QSqlQuery megolmSessionsQuery; + QSqlQuery megolmSessionsQuery(QSqlDatabase::database(QStringLiteral("Quotient"))); megolmSessionsQuery.prepare(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE matrixId=:matrixId;")); megolmSessionsQuery.bindValue(":matrixId", matrixId); - QSqlQuery groupSessionIndexRecordQuery; + QSqlQuery groupSessionIndexRecordQuery(QSqlDatabase::database(QStringLiteral("Quotient"))); groupSessionIndexRecordQuery.prepare(QStringLiteral("DELETE FROM group_session_record_index WHERE matrixId=:matrixId;")); groupSessionIndexRecordQuery.bindValue(":matrixId", matrixId); @@ -141,7 +141,7 @@ void Database::clear(const QString &matrixId) void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("INSERT INTO olm_sessions(matrixId, senderKey, sessionId, pickle) VALUES(:matrixId, :senderKey, :sessionId, :pickle);")); query.bindValue(":matrixId", matrixId); query.bindValue(":senderKey", senderKey); @@ -154,7 +154,7 @@ void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, UnorderedMap> Database::loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("SELECT * FROM olm_sessions WHERE matrixId=:matrixId;")); query.bindValue(":matrixId", matrixId); transaction(); @@ -174,7 +174,7 @@ UnorderedMap> Database::loadOlmSessions(con UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE matrixId=:matrixId AND roomId=:roomId;")); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); @@ -195,7 +195,7 @@ UnorderedMap, QOlmInboundGroupSessionPtr> Database::load void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("INSERT INTO inbound_megolm_sessions(matrixId, roomId, senderKey, sessionId, pickle) VALUES(:matrixId, :roomId, :senderKey, :sessionId, :pickle);")); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); @@ -209,7 +209,7 @@ void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare("INSERT INTO group_session_record_index(matrixId, roomId, sessionId, i, eventId, ts) VALUES(:matrixId, :roomId, :sessionId, :index, :eventId, :ts);"); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); @@ -224,7 +224,7 @@ void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString QPair Database::groupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, qint64 index) { - QSqlQuery query; + QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); query.prepare(QStringLiteral("SELECT * FROM group_session_record_index WHERE matrixId=:matrixId AND roomId=:roomId AND sessionId=:sessionId AND i=:index;")); query.bindValue(":matrixId", matrixId); query.bindValue(":roomId", roomId); -- cgit v1.2.3 From 7129118a7735a13af0db7d71efd60a330feac877 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 10 Dec 2021 00:12:39 +0100 Subject: ifdef more things --- lib/connection.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 433dd942..d1a29a7d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -39,6 +39,12 @@ # include "e2ee/qolmutils.h" # include "encryptionmanager.h" # include "database.h" + +#if QT_VERSION_MAJOR >= 6 +# include +#else +# include +#endif #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -55,12 +61,6 @@ #include #include -#if QT_VERSION_MAJOR >= 6 -# include -#else -# include -#endif - using namespace Quotient; -- cgit v1.2.3 From 6cec450f1d749936bd51a1471ac0ed74f633ef66 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 10 Dec 2021 00:26:03 +0100 Subject: Fix compilation of tests against older qt --- autotests/testgroupsession.cpp | 6 +++--- autotests/testolmaccount.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index afd5ef81..5024ccea 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -13,7 +13,7 @@ void TestOlmSession::groupSessionPicklingValid() { auto ogs = QOlmOutboundGroupSession::create(); const auto ogsId = ogs->sessionId(); - QVERIFY(QByteArray::fromBase64Encoding(ogsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QVERIFY(QByteArray::fromBase64(ogsId).size() > 0); QCOMPARE(0, ogs->sessionMessageIndex()); auto ogsPickled = std::get(ogs->pickle(Unencrypted {})); @@ -23,7 +23,7 @@ void TestOlmSession::groupSessionPicklingValid() auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); const auto igsId = igs->sessionId(); // ID is valid base64? - QVERIFY(QByteArray::fromBase64Encoding(igsId).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QVERIFY(QByteArray::fromBase64(igsId).size() > 0); //// no messages have been sent yet QCOMPARE(0, igs->firstKnownIndex()); @@ -42,7 +42,7 @@ void TestOlmSession::groupSessionCryptoValid() const auto plainText = QStringLiteral("Hello world!"); const auto ciphertext = std::get(ogs->encrypt(plainText)); // ciphertext valid base64? - QVERIFY(QByteArray::fromBase64Encoding(ciphertext).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0); const auto decryptionResult = std::get>(igs->decrypt(ciphertext)); diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 22c457aa..f0fcfe58 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -46,8 +46,8 @@ void TestOlmAccount::identityKeysValid() QCOMPARE(ed25519.size(), 43); // encoded as valid base64? - QVERIFY(QByteArray::fromBase64Encoding(curve25519).decodingStatus == QByteArray::Base64DecodingStatus::Ok); - QVERIFY(QByteArray::fromBase64Encoding(ed25519).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QVERIFY(QByteArray::fromBase64(curve25519).size() > 0); + QVERIFY(QByteArray::fromBase64(ed25519).size() > 0); } void TestOlmAccount::signatureValid() @@ -56,7 +56,7 @@ void TestOlmAccount::signatureValid() olmAccount.createNewAccount(); const auto message = "Hello world!"; const auto signature = olmAccount.sign(message); - QVERIFY(QByteArray::fromBase64Encoding(signature).decodingStatus == QByteArray::Base64DecodingStatus::Ok); + QVERIFY(QByteArray::fromBase64(signature).size() > 0); QOlmUtility utility; const auto identityKeys = olmAccount.identityKeys(); -- 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/connection.cpp | 21 ++++++--- lib/connection.h | 2 + lib/database.cpp | 113 +++++++++++++++++++++------------------------- lib/database.h | 32 ++++++------- lib/encryptionmanager.cpp | 9 ++-- lib/room.cpp | 8 ++-- 6 files changed, 93 insertions(+), 92 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d1a29a7d..8b9f9688 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -116,6 +116,7 @@ public: QueryKeysJob *currentQueryKeysJob = nullptr; bool encryptionUpdateRequired = false; PicklingMode picklingMode = Unencrypted {}; + Database *database = nullptr; #endif GetCapabilitiesJob* capabilitiesJob = nullptr; @@ -268,11 +269,9 @@ Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent), d(new Private(std::make_unique(server))) { #ifdef Quotient_E2EE_ENABLED - d->encryptionManager = new EncryptionManager(this); connect(qApp, &QCoreApplication::aboutToQuit, this, [this](){ saveOlmAccount(); }); - Database::instance(); #endif d->q = this; // All d initialization should occur before this line } @@ -446,7 +445,8 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED - Database::instance().clear(loginJob->userId()); + database = new Database(loginJob->userId(), q); + database->clear(); #endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { @@ -502,9 +502,13 @@ void Connection::Private::completeSetup(const QString& mxId) olmAccount = std::make_unique(data->userId(), data->deviceId(), q); connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); + if (!database) { + database = new Database(data->userId(), q); + } + encryptionManager = new EncryptionManager(q); - if (Database::instance().accountPickle(data->userId()).isEmpty()) { + if (database->accountPickle().isEmpty()) { // create new account and save unpickle data olmAccount->createNewAccount(); auto job = q->callApi(olmAccount->deviceKeys()); @@ -513,7 +517,7 @@ void Connection::Private::completeSetup(const QString& mxId) }); } else { // account already existing - auto pickle = Database::instance().accountPickle(data->userId()); + auto pickle = database->accountPickle(); olmAccount->unpickle(pickle, picklingMode); } #endif // Quotient_E2EE_ENABLED @@ -2040,7 +2044,7 @@ void Connection::saveOlmAccount() qCDebug(E2EE) << "Saving olm account"; #ifdef Quotient_E2EE_ENABLED auto pickle = d->olmAccount->pickle(d->picklingMode); - Database::instance().setAccountPickle(userId(), std::get(pickle)); + d->database->setAccountPickle(std::get(pickle)); #endif } @@ -2067,4 +2071,9 @@ QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) } return decrypted->fullJson(); } + +Database* Connection::database() +{ + return d->database; +} #endif diff --git a/lib/connection.h b/lib/connection.h index 3a12ec39..93ee496e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -47,6 +47,7 @@ class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; +class Database; class QOlmAccount; @@ -313,6 +314,7 @@ public: bool isLoggedIn() const; #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; + Database* database(); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/database.cpp b/lib/database.cpp index 01015d3c..41e62935 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -17,13 +17,16 @@ //TODO: delete room specific data when leaving room using namespace Quotient; -Database::Database() +Database::Database(const QString& matrixId, QObject* parent) + : QObject(parent) + , m_matrixId(matrixId) { - QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient")); - QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + m_matrixId.replace(':', '_'); + QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId)); + QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId); QDir(databasePath).mkpath(databasePath); - QSqlDatabase::database(QStringLiteral("Quotient")).setDatabaseName(databasePath + QStringLiteral("/quotient.db3")); - QSqlDatabase::database(QStringLiteral("Quotient")).open(); + database().setDatabaseName(databasePath + QStringLiteral("/quotient.db3")); + database().open(); switch(version()) { case 0: migrateTo1(); @@ -47,7 +50,7 @@ int Database::version() QSqlQuery Database::execute(const QString &queryString) { - auto query = QSqlDatabase::database(QStringLiteral("Quotient")).exec(queryString); + auto query = database().exec(queryString); if (query.lastError().type() != QSqlError::NoError) { qCritical() << "Failed to execute query"; qCritical() << query.lastQuery(); @@ -68,32 +71,30 @@ QSqlQuery Database::execute(QSqlQuery &query) void Database::transaction() { - QSqlDatabase::database(QStringLiteral("Quotient")).transaction(); + database().transaction(); } void Database::commit() { - QSqlDatabase::database(QStringLiteral("Quotient")).commit(); + database().commit(); } void Database::migrateTo1() { qCDebug(DATABASE) << "Migrating database to version 1"; transaction(); - execute(QStringLiteral("CREATE TABLE accounts (matrixId TEXT UNIQUE, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE olm_sessions (matrixId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (matrixId TEXT, roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE group_session_record_index (matrixId TEXT, roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); } -QByteArray Database::accountPickle(const QString &matrixId) +QByteArray Database::accountPickle() { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("SELECT pickle FROM accounts WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", matrixId); + auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); execute(query); if (query.next()) { return query.value(QStringLiteral("pickle")).toByteArray(); @@ -101,34 +102,23 @@ QByteArray Database::accountPickle(const QString &matrixId) return {}; } -void Database::setAccountPickle(const QString &matrixId, const QByteArray &pickle) +void Database::setAccountPickle(const QByteArray &pickle) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("INSERT INTO accounts(matrixId, pickle) VALUES(:matrixId, :pickle) ON CONFLICT (matrixId) DO UPDATE SET pickle=:pickle WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", matrixId); + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);")); query.bindValue(":pickle", pickle); transaction(); + execute(deleteQuery); execute(query); commit(); } -void Database::clear(const QString &matrixId) +void Database::clear() { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("DELETE FROM accounts WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", matrixId); - - QSqlQuery sessionsQuery(QSqlDatabase::database(QStringLiteral("Quotient"))); - sessionsQuery.prepare(QStringLiteral("DELETE FROM olm_sessions WHERE matrixId=:matrixId;")); - sessionsQuery.bindValue(":matrixId", matrixId); - - QSqlQuery megolmSessionsQuery(QSqlDatabase::database(QStringLiteral("Quotient"))); - megolmSessionsQuery.prepare(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE matrixId=:matrixId;")); - megolmSessionsQuery.bindValue(":matrixId", matrixId); - - QSqlQuery groupSessionIndexRecordQuery(QSqlDatabase::database(QStringLiteral("Quotient"))); - groupSessionIndexRecordQuery.prepare(QStringLiteral("DELETE FROM group_session_record_index WHERE matrixId=:matrixId;")); - groupSessionIndexRecordQuery.bindValue(":matrixId", matrixId); + auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;")); + auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;")); + auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;")); transaction(); execute(query); @@ -139,11 +129,9 @@ void Database::clear(const QString &matrixId) } -void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle) +void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("INSERT INTO olm_sessions(matrixId, senderKey, sessionId, pickle) VALUES(:matrixId, :senderKey, :sessionId, :pickle);")); - query.bindValue(":matrixId", matrixId); + auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle) VALUES(:senderKey, :sessionId, :pickle);")); query.bindValue(":senderKey", senderKey); query.bindValue(":sessionId", sessionId); query.bindValue(":pickle", pickle); @@ -152,11 +140,9 @@ void Database::saveOlmSession(const QString& matrixId, const QString& senderKey, commit(); } -UnorderedMap> Database::loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode) +UnorderedMap> Database::loadOlmSessions(const PicklingMode& picklingMode) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("SELECT * FROM olm_sessions WHERE matrixId=:matrixId;")); - query.bindValue(":matrixId", matrixId); + QSqlQuery query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); transaction(); execute(query); commit(); @@ -172,11 +158,9 @@ UnorderedMap> Database::loadOlmSessions(con return sessions; } -UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode) +UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE matrixId=:matrixId AND roomId=:roomId;")); - query.bindValue(":matrixId", matrixId); + auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;")); query.bindValue(":roomId", roomId); transaction(); execute(query); @@ -193,11 +177,9 @@ UnorderedMap, QOlmInboundGroupSessionPtr> Database::load return sessions; } -void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("INSERT INTO inbound_megolm_sessions(matrixId, roomId, senderKey, sessionId, pickle) VALUES(:matrixId, :roomId, :senderKey, :sessionId, :pickle);")); - query.bindValue(":matrixId", matrixId); + auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, pickle) VALUES(:roomId, :senderKey, :sessionId, :pickle);")); query.bindValue(":roomId", roomId); query.bindValue(":senderKey", senderKey); query.bindValue(":sessionId", sessionId); @@ -207,11 +189,9 @@ void Database::saveMegolmSession(const QString& matrixId, const QString& roomId, commit(); } -void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) +void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare("INSERT INTO group_session_record_index(matrixId, roomId, sessionId, i, eventId, ts) VALUES(:matrixId, :roomId, :sessionId, :index, :eventId, :ts);"); - query.bindValue(":matrixId", matrixId); + QSqlQuery query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"); query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); query.bindValue(":index", index); @@ -222,11 +202,10 @@ void Database::addGroupSessionIndexRecord(const QString& matrixId, const QString commit(); } -QPair Database::groupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, qint64 index) +QPair Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) { - QSqlQuery query(QSqlDatabase::database(QStringLiteral("Quotient"))); - query.prepare(QStringLiteral("SELECT * FROM group_session_record_index WHERE matrixId=:matrixId AND roomId=:roomId AND sessionId=:sessionId AND i=:index;")); - query.bindValue(":matrixId", matrixId); + QSqlQuery query(database()); + query.prepare(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); query.bindValue(":index", index); @@ -238,3 +217,15 @@ QPair Database::groupSessionIndexRecord(const QString& matrixId } return {query.value("eventId").toString(), query.value("ts").toLongLong()}; } + +QSqlDatabase Database::database() +{ + return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId)); +} + +QSqlQuery Database::prepareQuery(const QString& queryString) +{ + QSqlQuery query(database()); + query.prepare(queryString); + return query; +} diff --git a/lib/database.h b/lib/database.h index 25af2833..fbb940c8 100644 --- a/lib/database.h +++ b/lib/database.h @@ -15,32 +15,28 @@ class Database : public QObject Q_OBJECT public: - static Database &instance() - { - static Database _instance; - return _instance; - } + Database(const QString& matrixId, QObject* parent); int version(); void transaction(); void commit(); QSqlQuery execute(const QString &queryString); QSqlQuery execute(QSqlQuery &query); - - QByteArray accountPickle(const QString &matrixId); - void setAccountPickle(const QString &matrixId, const QByteArray &pickle); - void clear(const QString &matrixId); - void saveOlmSession(const QString& matrixId, const QString& senderKey, const QString& sessionId, const QByteArray &pickle); - UnorderedMap> loadOlmSessions(const QString& matrixId, const PicklingMode& picklingMode); - UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& matrixId, const QString& roomId, const PicklingMode& picklingMode); - void saveMegolmSession(const QString& matrixId, const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); - void addGroupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); - QPair groupSessionIndexRecord(const QString& matrixId, const QString& roomId, const QString& sessionId, qint64 index); - + QSqlDatabase database(); + QSqlQuery prepareQuery(const QString& quaryString); + + QByteArray accountPickle(); + void setAccountPickle(const QByteArray &pickle); + void clear(); + void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle); + UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); + UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); + void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); + QPair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); private: - Database(); - void migrateTo1(); + QString m_matrixId; }; } diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index c0e44f70..abdcdcee 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -33,19 +33,21 @@ class EncryptionManager::Private { public: EncryptionManager* q; + Connection* connection; + // A map from SenderKey to vector of InboundSession UnorderedMap> sessions; void loadSessions() { - sessions = Database::instance().loadOlmSessions(static_cast(q->parent())->userId(), static_cast(q->parent())->picklingMode()); + sessions = connection->database()->loadOlmSessions(connection->picklingMode()); } void saveSession(QOlmSessionPtr& session, const QString &senderKey) { - auto pickleResult = session->pickle(static_cast(q->parent())->picklingMode()); + auto pickleResult = session->pickle(connection->picklingMode()); if (std::holds_alternative(pickleResult)) { qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get(pickleResult); return; } - Database::instance().saveOlmSession(static_cast(q->parent())->userId(), senderKey, session->sessionId(), std::get(pickleResult)); + connection->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult)); } QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { @@ -103,6 +105,7 @@ EncryptionManager::EncryptionManager(QObject* parent) , d(std::make_unique()) { d->q = this; + d->connection = static_cast(parent); d->loadSessions(); } 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 b4a6070d44140a3cbc931b18530721e31f069455 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Fri, 10 Dec 2021 16:15:50 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/events/roomevent.cpp | 2 +- lib/events/roomevent.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index dbce2255..eb5d0485 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -128,7 +128,7 @@ CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) } #ifdef Quotient_E2EE_ENABLED -void RoomEvent::setOriginalEvent(event_ptr_tt originalEvent) +void RoomEvent::setOriginalEvent(event_ptr_tt&& originalEvent) { _originalEvent = std::move(originalEvent); } diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 36b45f09..3d46bf9b 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -61,7 +61,8 @@ public: void addId(const QString& newId); #ifdef Quotient_E2EE_ENABLED - void setOriginalEvent(event_ptr_tt originalEvent); + void setOriginalEvent(event_ptr_tt&& originalEvent); + const RoomEvent* originalEvent() { return _originalEvent.get(); } const QJsonObject encryptedJson() const; #endif -- 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(-) 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/database.cpp | 14 ++++++++++++-- lib/database.h | 1 + lib/room.cpp | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 41e62935..665b931a 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -14,8 +14,6 @@ #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" -//TODO: delete room specific data when leaving room - using namespace Quotient; Database::Database(const QString& matrixId, QObject* parent) : QObject(parent) @@ -229,3 +227,15 @@ QSqlQuery Database::prepareQuery(const QString& queryString) query.prepare(queryString); return query; } + +void Database::clearRoomData(const QString& roomId) +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;")); + auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;")); + transaction(); + execute(query); + execute(query2); + execute(query3); + commit(); +} diff --git a/lib/database.h b/lib/database.h index fbb940c8..b2187ba4 100644 --- a/lib/database.h +++ b/lib/database.h @@ -34,6 +34,7 @@ public: void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); QPair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); + void clearRoomData(const QString& roomId); private: void migrateTo1(); 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 e5256e0b1e4c43ce96d99d1b82ca5d98a1baded6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 17 Dec 2021 08:07:07 +0100 Subject: RoomMemberEvent: fix an off-by-one error Also: extended quotest to cover member renames, not just user profile renames. --- lib/events/roommemberevent.cpp | 8 +++----- quotest/quotest.cpp | 35 +++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index b0bc7bcb..3141f6b5 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -48,11 +48,9 @@ void MemberEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); if (membership != Membership::Invalid) - o->insert( - QStringLiteral("membership"), - MembershipStrings[qCountTrailingZeroBits( - std::underlying_type_t(membership)) - + 1]); + o->insert(QStringLiteral("membership"), + MembershipStrings[qCountTrailingZeroBits( + std::underlying_type_t(membership))]); if (displayName) o->insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 764d5dfd..8703efb2 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -214,6 +214,7 @@ TestManager::TestManager(int& argc, char** argv) // Big countdown watchdog QTimer::singleShot(180000, this, [this] { + clog << "Time is up, stopping the session"; if (testSuite) conclude(); else @@ -537,14 +538,32 @@ TEST_IMPL(setTopic) TEST_IMPL(changeName) { - auto* const localUser = connection()->user(); - const auto& newName = connection()->generateTxnId(); // See setTopic() - clog << "Renaming the user to " << newName.toStdString() << endl; - localUser->rename(newName); - connectUntil(localUser, &User::defaultNameChanged, this, - [this, thisTest, localUser, newName] { - FINISH_TEST(localUser->name() == newName); - }); + connectSingleShot(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { + auto* const localUser = connection()->user(); + const auto& newName = connection()->generateTxnId(); // See setTopic() + clog << "Renaming the user to " << newName.toStdString() + << " in the target room" << endl; + localUser->rename(newName, targetRoom); + connectUntil(targetRoom, &Room::memberRenamed, this, + [this, thisTest, localUser, newName](const User* u) { + if (localUser != u) + return false; + if (localUser->name(targetRoom) != newName) + FAIL_TEST(); + + clog + << "Member rename successful, renaming the account" + << endl; + const auto newN = newName.mid(0, 5); + localUser->rename(newN); + connectUntil(localUser, &User::defaultNameChanged, + this, [this, thisTest, localUser, newN] { + targetRoom->localUser()->rename({}); + FINISH_TEST(localUser->name() == newN); + }); + return true; + }); + }); return false; } -- cgit v1.2.3 From 24a562cc64e8fac6c55108ae2b7b6997ecdd2010 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 19 Dec 2021 10:43:33 +0100 Subject: Quotest: add a missing \n in the output --- quotest/quotest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 8703efb2..7bd9c5c3 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -214,7 +214,7 @@ TestManager::TestManager(int& argc, char** argv) // Big countdown watchdog QTimer::singleShot(180000, this, [this] { - clog << "Time is up, stopping the session"; + clog << "Time is up, stopping the session\n"; if (testSuite) conclude(); else -- cgit v1.2.3 From 487e7f5ef75a5a5a4d732027d0b7705fdffb71ca Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 20 Dec 2021 16:18:35 +0100 Subject: Add event_loader.h to CMakeLists Another forgotten header file. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2762df6a..7675daad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ list(APPEND lib_SRCS lib/accountregistry.h lib/accountregistry.cpp lib/mxcreply.h lib/mxcreply.cpp lib/events/event.h lib/events/event.cpp + lib/events/eventloader.h lib/events/roomevent.h lib/events/roomevent.cpp lib/events/stateevent.h lib/events/stateevent.cpp lib/events/simplestateevents.h -- cgit v1.2.3 From b989383165b648269a231d1febadc8150676d5cf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 21 Dec 2021 18:40:47 +0100 Subject: Don't chain RoomEvent to Event factory any more Objects derived from Event are not room events (in the spec sense) and never occur in the same arrays as room events; therefore this chaining has always been superfluous. --- lib/events/roomevent.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index fb921af6..b728e0bf 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -9,9 +9,6 @@ using namespace Quotient; -[[maybe_unused]] static auto roomEventTypeInitialised = - Event::factory_t::chainFactory(); - RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : Event(type, matrixType, contentJson) -- cgit v1.2.3 From 231c4d723eb53f3ea5f641b743d198584840a963 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Dec 2021 13:44:39 +0100 Subject: Simplify the code around EventFactory<> The former code assumed that EventFactory<> is just a class-level shell for a bunch of functions and a static data member that only exists to allow specialisations to occur for the whole group together. On top of that, setupFactory() and registerEventType() strived to protect this group from double registration coming from static variables in an anonymous namespace produced by REGISTER_EVENT_TYPE. The whole thing is now de-static-ed: resolving the factory now relies on class-static Event/RoomEvent/StateEventBase::factory variables instead of factory_t type aliases; and REGISTER_EVENT_TYPE produces non-static inline variables instead, obviating the need of registerEventType/setupFactory kludge. --- lib/events/event.h | 166 +++++++++++++++++++++++-------------------- lib/events/eventloader.h | 18 ++--- lib/events/roomevent.h | 2 +- lib/events/roommemberevent.h | 32 ++++----- lib/events/stateevent.cpp | 15 ---- lib/events/stateevent.h | 18 ++++- 6 files changed, 125 insertions(+), 126 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 4d4bb16b..e786fb30 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -110,94 +110,90 @@ inline event_type_t typeId() inline event_type_t unknownEventTypeId() { return typeId(); } -// === EventFactory === +// === Event creation facilities === -/** Create an event of arbitrary type from its arguments */ +//! Create an event of arbitrary type from its arguments template inline event_ptr_tt makeEvent(ArgTs&&... args) { return std::make_unique(std::forward(args)...); } -template -class EventFactory { -public: - template - static auto addMethod(FnT&& method) - { - factories().emplace_back(std::forward(method)); - return 0; - } - - /** Chain two type factories - * Adds the factory class of EventT2 (EventT2::factory_t) to - * the list in factory class of EventT1 (EventT1::factory_t) so - * that when EventT1::factory_t::make() is invoked, types of - * EventT2 factory are looked through as well. This is used - * to include RoomEvent types into the more general Event factory, - * and state event types into the RoomEvent factory. - */ - template - static auto chainFactory() - { - return addMethod(&EventT::factory_t::make); - } - - static event_ptr_tt make(const QJsonObject& json, - const QString& matrixType) - { - for (const auto& f : factories()) - if (auto e = f(json, matrixType)) - return e; - return nullptr; - } - -private: - static auto& factories() +namespace _impl { + template + event_ptr_tt makeIfMatches(const QJsonObject& json, + const QString& matrixType) { - using inner_factory_tt = std::function( - const QJsonObject&, const QString&)>; - static std::vector _factories {}; - return _factories; + return QLatin1String(EventT::matrixTypeId()) == matrixType + ? makeEvent(json) + : nullptr; } -}; - -/** Add a type to its default factory - * Adds a standard factory method (via makeEvent<>) for a given - * type to EventT::factory_t factory class so that it can be - * created dynamically from loadEvent<>(). - * - * \tparam EventT the type to enable dynamic creation of - * \return the registered type id - * \sa loadEvent, Event::type - */ -template -inline auto setupFactory() -{ - qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); - return EventT::factory_t::addMethod([](const QJsonObject& json, - const QString& jsonMatrixType) { - return EventT::matrixTypeId() == jsonMatrixType ? makeEvent(json) - : nullptr; - }); -} -template -inline auto registerEventType() -{ - // Initialise exactly once, even if this function is called twice for - // the same type (for whatever reason - you never know the ways of - // static initialisation is done). - static const auto _ = setupFactory(); - return _; // Only to facilitate usage in static initialisation -} + //! \brief A family of event factories to create events from CS API responses + //! + //! Each of these factories, as instantiated by event base types (Event, + //! RoomEvent etc.) is capable of producing an event object derived from + //! \p BaseEventT, using the JSON payload and the event type passed to its + //! make() method. Don't use these directly to make events; use loadEvent() + //! overloads as the frontend for these. Never instantiate new factories + //! outside of base event classes. + //! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory, + //! StateEventBase::factory + template + class EventFactory + : private std::vector (*)(const QJsonObject&, + const QString&)> { + // Actual makeIfMatches specialisations will differ in the first + // template parameter but that doesn't affect the function type + public: + explicit EventFactory(const char* name = "") + : name(name) + { + static auto yetToBeConstructed = true; + Q_ASSERT(yetToBeConstructed); + if (!yetToBeConstructed) // For Release builds that pass Q_ASSERT + qCritical(EVENTS) + << "Another EventFactory for the same base type is being " + "created - event creation logic will be splintered"; + yetToBeConstructed = false; + } + EventFactory(const EventFactory&) = delete; + + //! \brief Add a method to create events of a given type + //! + //! Adds a standard factory method (makeIfMatches) for \p EventT so that + //! event objects of this type can be created dynamically by loadEvent. + //! The caller is responsible for ensuring this method is called only + //! once per type. + //! \sa makeIfMatches, loadEvent, Quotient::loadEvent + template + bool addMethod() + { + this->emplace_back(&makeIfMatches); + qDebug(EVENTS) << "Added factory method for" + << EventT::matrixTypeId() << "events;" << this->size() + << "methods in the" << name << "chain by now"; + return true; + } + + auto loadEvent(const QJsonObject& json, const QString& matrixType) + { + for (const auto& f : *this) + if (auto e = f(json, matrixType)) + return e; + return makeEvent(unknownEventTypeId(), json); + } + + const char* const name; + }; +} // namespace _impl // === Event === class Event { public: using Type = event_type_t; - using factory_t = EventFactory; + static inline _impl::EventFactory factory { "Event" }; explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, @@ -272,7 +268,7 @@ template using EventsArray = std::vector>; using Events = EventsArray; -// === Macros used with event class definitions === +// === Facilities for event class definitions === // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). @@ -284,13 +280,27 @@ using Events = EventsArray; // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphic event arrays -#define REGISTER_EVENT_TYPE(_Type) \ - namespace { \ - [[maybe_unused]] static const auto _factoryAdded##_Type = \ - registerEventType<_Type>(); \ - } \ +#define REGISTER_EVENT_TYPE(_Type) \ + [[maybe_unused]] inline const auto _factoryAdded##_Type = \ + _Type::factory.addMethod<_Type>(); \ // End of macro +// === Event loading === +// (see also event_loader.h) + +//! \brief Point of customisation to dynamically load events +//! +//! The default specialisation of this calls BaseEventT::factory and if that +//! fails (i.e. returns nullptr) creates an unknown event of BaseEventT. +//! Other specialisations may reuse other factories, add validations common to +//! BaseEventT, and so on +template +event_ptr_tt doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + return BaseEventT::factory.loadEvent(json, matrixType); +} + // === is<>(), eventCast<>() and switchOnType<>() === template diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 978668f2..fe624d70 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -6,16 +6,6 @@ #include "stateevent.h" namespace Quotient { -namespace _impl { - template - static inline auto loadEvent(const QJsonObject& json, - const QString& matrixType) - { - if (auto e = EventFactory::make(json, matrixType)) - return e; - return makeEvent(unknownEventTypeId(), json); - } -} // namespace _impl /*! Create an event with proper type from a JSON object * @@ -26,7 +16,7 @@ namespace _impl { template inline event_ptr_tt loadEvent(const QJsonObject& fullJson) { - return _impl::loadEvent(fullJson, fullJson[TypeKeyL].toString()); + return doLoadEvent(fullJson, fullJson[TypeKeyL].toString()); } /*! Create an event from a type string and content JSON @@ -39,8 +29,8 @@ template inline event_ptr_tt loadEvent(const QString& matrixType, const QJsonObject& content) { - return _impl::loadEvent(basicEventJson(matrixType, content), - matrixType); + return doLoadEvent(basicEventJson(matrixType, content), + matrixType); } /*! Create a state event from a type string, content JSON and state key @@ -53,7 +43,7 @@ inline StateEventPtr loadStateEvent(const QString& matrixType, const QJsonObject& content, const QString& stateKey = {}) { - return _impl::loadEvent( + return doLoadEvent( basicStateEventJson(matrixType, content, stateKey), matrixType); } diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 7f13f6f2..8be58481 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -13,7 +13,7 @@ class RedactionEvent; /** This class corresponds to m.room.* events */ class RoomEvent : public Event { public: - using factory_t = EventFactory; + static inline _impl::EventFactory factory { "RoomEvent" }; // RedactionEvent is an incomplete type here so we cannot inline // constructors and destructors and we cannot use 'using'. diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index f3047159..0fb464d4 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -49,16 +49,15 @@ public: std::forward(contentArgs)...) {} - /// A special constructor to create unknown RoomMemberEvents - /** - * This is needed in order to use RoomMemberEvent as a "base event - * class" in cases like GetMembersByRoomJob when RoomMemberEvents - * (rather than RoomEvents or StateEvents) are resolved from JSON. - * For such cases loadEvent<> requires an underlying class to be - * constructible with unknownTypeId() instead of its genuine id. - * Don't use it directly. - * \sa GetMembersByRoomJob, loadEvent, unknownTypeId - */ + //! \brief A special constructor to create unknown RoomMemberEvents + //! + //! This is needed in order to use RoomMemberEvent as a "base event class" + //! in cases like GetMembersByRoomJob when RoomMemberEvents (rather than + //! RoomEvents or StateEvents) are resolved from JSON. For such cases + //! loadEvent\<> requires an underlying class to have a specialisation of + //! EventFactory\<> and be constructible with unknownTypeId() instead of + //! its genuine id. Don't use directly. + //! \sa EventFactory, loadEvent, GetMembersByRoomJob RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) {} @@ -89,14 +88,13 @@ public: }; template <> -class EventFactory { -public: - static event_ptr_tt make(const QJsonObject& json, - const QString&) - { +inline event_ptr_tt +doLoadEvent(const QJsonObject& json, const QString& matrixType) +{ + if (matrixType == QLatin1String(RoomMemberEvent::matrixTypeId())) return makeEvent(json); - } -}; + return makeEvent(unknownEventTypeId(), json); +} REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index efe011a0..9535af56 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -5,21 +5,6 @@ using namespace Quotient; -// Aside from the normal factory to instantiate StateEventBase inheritors -// StateEventBase itself can be instantiated if there's a state_key JSON key -// but the event type is unknown. -[[maybe_unused]] static auto stateEventTypeInitialised = - RoomEvent::factory_t::addMethod( - [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains(StateKeyKeyL)) - return nullptr; - - if (auto e = StateEventBase::factory_t::make(json, matrixType)) - return e; - - return makeEvent(unknownEventTypeId(), json); - }); - StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index b0aa9907..919e8f86 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -19,7 +19,7 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, class StateEventBase : public RoomEvent { public: - using factory_t = EventFactory; + static inline _impl::EventFactory factory { "StateEvent" }; StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) {} @@ -37,6 +37,22 @@ public: using StateEventPtr = event_ptr_tt; using StateEvents = EventsArray; +//! \brief Override RoomEvent factory with that from StateEventBase if JSON has +//! stateKey +//! +//! This means in particular that an event with a type known to RoomEvent but +//! having stateKey set (even to an empty value) will be treated as a state +//! event and most likely end up as unknown (consider, e.g., m.room.message +//! that has stateKey set). +template <> +inline RoomEventPtr doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + if (json.contains(StateKeyKeyL)) + return StateEventBase::factory.loadEvent(json, matrixType); + return RoomEvent::factory.loadEvent(json, matrixType); +} + template <> inline bool is(const Event& e) { -- cgit v1.2.3 From 060af9334049c58767b6457da2d7c07fdb0d171e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Dec 2021 16:13:02 +0100 Subject: StateEventBase: force type to unknown if stateKey is not in JSON --- lib/events/stateevent.cpp | 8 ++++++++ lib/events/stateevent.h | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 9535af56..e53d47d4 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -5,6 +5,14 @@ using namespace Quotient; +StateEventBase::StateEventBase(Type type, const QJsonObject& json) + : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json) +{ + if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL)) + qWarning(EVENTS) << "Attempt to create a state event with no stateKey -" + "forcing the event type to unknown to avoid damage"; +} + StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 919e8f86..c37965aa 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -21,8 +21,7 @@ class StateEventBase : public RoomEvent { public: static inline _impl::EventFactory factory { "StateEvent" }; - StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) - {} + StateEventBase(Type type, const QJsonObject& json); StateEventBase(Type type, event_mtype_t matrixType, const QString& stateKey = {}, const QJsonObject& contentJson = {}); -- cgit v1.2.3 From 79841d6add9e60716ec6690cde3bccf952cceada Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 22 Dec 2021 19:13:36 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/database.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 665b931a..d4365647 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -140,7 +140,7 @@ void Database::saveOlmSession(const QString& senderKey, const QString& sessionId UnorderedMap> Database::loadOlmSessions(const PicklingMode& picklingMode) { - QSqlQuery query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); transaction(); execute(query); commit(); @@ -189,7 +189,7 @@ void Database::saveMegolmSession(const QString& roomId, const QString& senderKey void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) { - QSqlQuery query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"); + auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"); query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); query.bindValue(":index", index); -- cgit v1.2.3 From 5920f8cf64b60a07ddf73852d6d4f724ab3bb03a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 22 Dec 2021 19:16:49 +0100 Subject: Another improvement --- lib/database.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index d4365647..a5df22af 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -202,8 +202,7 @@ void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& QPair Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) { - QSqlQuery query(database()); - query.prepare(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); + auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); query.bindValue(":index", index); -- cgit v1.2.3 From 1176ec1eedb749e81e3d446733c267a971feefa4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 24 Dec 2021 00:03:32 +0100 Subject: Find sql when using libquotient --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43fed3e9..1ff65282 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -328,7 +328,8 @@ if (${PROJECT_NAME}_ENABLE_E2EE) ${Qt}::Sql ${QTKEYCHAIN_LIBRARIES}) set(FIND_DEPS "find_dependency(Olm) - find_dependency(OpenSSL)") # For QuotientConfig.cmake.in + find_dependency(OpenSSL) + find_dependency(${Qt}Sql)") # For QuotientConfig.cmake.in endif() target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) -- cgit v1.2.3 From 31bb962f36c31621b311f1aee654e36ea09e8d77 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 24 Dec 2021 01:47:52 +0100 Subject: Fix reading unencrypted images --- lib/mxcreply.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 2ad49c2c..c7f27b0c 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -29,6 +29,7 @@ public: MxcReply::MxcReply(QNetworkReply* reply) : d(std::make_unique(reply)) { + d->m_device = d->m_reply; reply->setParent(this); connect(d->m_reply, &QNetworkReply::finished, this, [this]() { setError(d->m_reply->error(), d->m_reply->errorString()); -- cgit v1.2.3 From 42eaf3671c656088f8d038f83973f0931ad7051c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 02:33:39 +0100 Subject: Sonar: leave just one job There's not much value in analysing the code without E2EE and with E2EE because E2EE is additive; and there's no plan to look close into the generated API code apart from what already ends up being committed. --- .github/workflows/sonar.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 76db59c9..ae96aebc 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -16,8 +16,8 @@ jobs: fail-fast: false matrix: qt-version: [ '5.12.10' ] - e2ee: [ '', 'e2ee' ] - update-api: [ '', 'update-api' ] + e2ee: [ 'e2ee' ] + update-api: [ '' ] env: SONAR_SCANNER_VERSION: 4.6.2.2472 -- cgit v1.2.3 From a08348007dd0a73a40b7b1d755b3affc963b5b80 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Dec 2021 17:31:37 +0100 Subject: Sonar: add coverage analysis --- .github/workflows/sonar.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index ae96aebc..c8ddca66 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -53,7 +53,8 @@ jobs: echo "CXX=g++-10" >>$GITHUB_ENV mkdir -p $HOME/.sonar echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ - -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV + -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" \ + -DCMAKE_CXX_FLAGS=--coverage >>$GITHUB_ENV cmake -E make_directory ${{ runner.workspace }}/build - name: Build and install olm @@ -110,4 +111,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | - $HOME/.sonar/sonar-scanner*/bin/sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" + $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ + -Dsonar.host.url="${{ env.SONAR_SERVER_URL }}" \ + -Dsonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" \ + -Dsonar.cfamily.gcov.reportsPath=build/coverage -- cgit v1.2.3 From 0cbbae133d61ccb1fbb41a40660a70c65f65235f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 23 Dec 2021 11:43:01 +0100 Subject: Merge Sonar invocation back to ci.yml For coverage analysis to work, a test run is needed, making the overlap between ci.yaml and sonar.yml quite significant again. Note: 'update-api' option is temporarily dropped from the matrix to speed up the check. If things run fine, 'update-api' will come back. --- .github/workflows/ci.yml | 59 +++++++++++++++++++--- .github/workflows/sonar.yml | 117 -------------------------------------------- 2 files changed, 51 insertions(+), 125 deletions(-) delete mode 100644 .github/workflows/sonar.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c78a5981..01a4468f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,21 @@ jobs: matrix: os: [ubuntu-20.04, macos-10.15] compiler: [ GCC, Clang ] - platform: [ '' ] qt-version: [ '5.12.10' ] - qt-arch: [ '' ] # Not using binary values here, to make the job captions more readable e2ee: [ '' ] - update-api: [ '', 'update-api' ] + update-api: [ '' ] #, 'update-api' ] + sonar: [ '' ] + platform: [ '' ] + qt-arch: [ '' ] exclude: - os: macos-10.15 compiler: GCC include: + - os: ubuntu-latest + compiler: GCC + qt-version: '5.12.10' + sonar: 'sonar' - os: windows-2019 compiler: MSVC platform: x64 @@ -43,6 +48,9 @@ jobs: qt-arch: win64_msvc2017_64 update-api: update-api + env: + SONAR_SERVER_URL: 'https://sonarcloud.io' + steps: - uses: actions/checkout@v2 with: @@ -96,9 +104,18 @@ jobs: VERSION="$(git describe --all --contains)-ci${{ github.run_number }}-$(git rev-parse --short HEAD)" fi echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ - -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local\ - -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" >>$GITHUB_ENV + + CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_SHARED_LIBS=false \ + -DCMAKE_INSTALL_PREFIX=~/.local \ + -DCMAKE_PREFIX_PATH=~/.local \ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" + + if [ -n "${{ matrix.sonar }}" ]; then + mkdir -p $HOME/.sonar + CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CXX_FLAGS=--coverage" + fi + echo "CMAKE_ARGS=$CMAKE_ARGS" >>$GITHUB_ENV if [[ '${{ runner.os }}' != 'Windows' ]]; then BIN_DIR=/bin @@ -115,6 +132,20 @@ jobs: with: arch: ${{ matrix.platform }} + - name: Download and set up Sonar Cloud tools + if: matrix.sonar != '' + env: + SONAR_SCANNER_VERSION: 4.6.2.2472 + run: | + pushd $HOME/.sonar + curl -sSL --remote-name-all \ + $SONAR_SERVER_URL/static/cpp/build-wrapper-linux-x86.zip \ + https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip + unzip -o build-wrapper*.zip + echo "BUILD_WRAPPER=$HOME/.sonar/build-wrapper-linux-x86/build-wrapper-linux* --out-dir $BUILD_PATH/sonar" >>$GITHUB_ENV + unzip -o sonar-scanner-cli*.zip + popd + - name: Build and install olm if: matrix.e2ee working-directory: ${{ runner.workspace }} @@ -158,7 +189,8 @@ jobs: - name: Build and install libQuotient run: | - cmake --build $BUILD_PATH --target all install + $BUILD_WRAPPER cmake --build $BUILD_PATH --target all + cmake --build $BUILD_PATH --target install ls ~/.local$BIN_DIR/quotest - name: Run tests @@ -172,7 +204,18 @@ jobs: [[ -z "$TEST_USER" ]] || \ $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually - + - name: Perform CodeQL analysis if: env.CODEQL_ANALYSIS uses: github/codeql-action/analyze@v1 + + - name: Run sonar-scanner + if: matrix.sonar != '' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ + -Dsonar.host.url="$SONAR_SERVER_URL" \ + -Dsonar.cfamily.build-wrapper-output="$BUILD_PATH/sonar" \ + -Dsonar.cfamily.gcov.reportsPath="$BUILD_PATH/coverage" diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index c8ddca66..00000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: Sonar - -on: - push: - pull_request: - types: [opened, reopened] - -defaults: - run: - shell: bash - -jobs: - SonarCloud: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - qt-version: [ '5.12.10' ] - e2ee: [ 'e2ee' ] - update-api: [ '' ] - - env: - SONAR_SCANNER_VERSION: 4.6.2.2472 - SONAR_SERVER_URL: "https://sonarcloud.io" - BUILD_WRAPPER_OUT_DIR: build/sonar - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: ${{ matrix.e2ee != '' }} - - - name: Cache Qt - id: cache-qt - uses: actions/cache@v2 - with: - path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}-Qt${{ matrix.qt-version }}-cache - - - name: Install Qt - uses: jurplel/install-qt-action@v2.11.1 - with: - version: ${{ matrix.qt-version }} -# arch: ${{ matrix.qt-arch }} # Only Windows needs that - cached: ${{ steps.cache-qt.outputs.cache-hit }} - - - name: Install Ninja - uses: seanmiddleditch/gha-setup-ninja@v3 - - - name: Setup build environment - run: | - echo "CC=gcc-10" >>$GITHUB_ENV - echo "CXX=g++-10" >>$GITHUB_ENV - mkdir -p $HOME/.sonar - echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ - -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" \ - -DCMAKE_CXX_FLAGS=--coverage >>$GITHUB_ENV - cmake -E make_directory ${{ runner.workspace }}/build - - - name: Build and install olm - if: matrix.e2ee - run: | - cd .. - git clone https://gitlab.matrix.org/matrix-org/olm.git - cmake -S olm -B olm/build $CMAKE_ARGS - cmake --build olm/build --target install - - - name: Pull CS API and build GTAD - if: matrix.update-api - run: | - cd .. - git clone https://github.com/quotient-im/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF - cmake --build gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ - -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ - >>$GITHUB_ENV - - - name: Download and set up Sonar Cloud tools - run: | - pushd $HOME/.sonar - curl -sSLo build-wrapper.zip $SONAR_SERVER_URL/static/cpp/build-wrapper-linux-x86.zip - unzip -o build-wrapper.zip - echo "BUILD_WRAPPER=$HOME/.sonar/build-wrapper-linux-x86/build-wrapper-linux* --out-dir $BUILD_WRAPPER_OUT_DIR" >>$GITHUB_ENV - curl -sSLo sonar-scanner.zip \ - https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip - unzip -o sonar-scanner.zip - popd - - - name: Configure libQuotient - run: | - if [[ '${{ runner.os }}' == 'Windows' ]]; then - BIN_DIR=. - else - BIN_DIR=bin - fi - echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV - cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} - - - name: Regenerate API code - if: matrix.update-api - run: cmake --build build --target update-api - - - name: Build libQuotient - run: | - $BUILD_WRAPPER cmake --build build --target all - - - name: Run sonar-scanner - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ - -Dsonar.host.url="${{ env.SONAR_SERVER_URL }}" \ - -Dsonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" \ - -Dsonar.cfamily.gcov.reportsPath=build/coverage -- cgit v1.2.3 From 669ed9bcae110ca0539876193ec3b8cb9b8a8a18 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 02:35:44 +0100 Subject: Actually do and submit coverage ...instead of hoping the thing will sort itself out because CLion does. --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01a4468f..8596ccd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,7 @@ jobs: if [ -n "${{ matrix.sonar }}" ]; then mkdir -p $HOME/.sonar CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CXX_FLAGS=--coverage" + echo "COV=gcov$CXX_VERSION_POSTFIX" >>$GITHUB_ENV fi echo "CMAKE_ARGS=$CMAKE_ARGS" >>$GITHUB_ENV @@ -215,7 +216,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | + mkdir .coverage && pushd .coverage + find $BUILD_PATH/CMakeFiles/Quotient.dir/lib -name '*.gcda' -print0 \ + | xargs -0 $COV -s $GITHUB_WORKSPACE/lib -pr + popd $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ -Dsonar.host.url="$SONAR_SERVER_URL" \ -Dsonar.cfamily.build-wrapper-output="$BUILD_PATH/sonar" \ - -Dsonar.cfamily.gcov.reportsPath="$BUILD_PATH/coverage" + -Dsonar.cfamily.gcov.reportsPath=.coverage -- cgit v1.2.3 From 3fa5925ad6c9ce558dd9726a02614fd6f9c031e5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 02:35:56 +0100 Subject: Analyse in 2 threads --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8596ccd5..47c92ad3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,4 +223,5 @@ jobs: $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ -Dsonar.host.url="$SONAR_SERVER_URL" \ -Dsonar.cfamily.build-wrapper-output="$BUILD_PATH/sonar" \ + -Dsonar.cfamily.threads=2 \ -Dsonar.cfamily.gcov.reportsPath=.coverage -- cgit v1.2.3 From e1c19404846b2a7a2f321c662528252e4eeef35e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 06:42:36 +0100 Subject: Don't strip lib from names in .gcov files That apparently confuses Sonar as it fails to match the source files. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47c92ad3..27c65b26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: run: | mkdir .coverage && pushd .coverage find $BUILD_PATH/CMakeFiles/Quotient.dir/lib -name '*.gcda' -print0 \ - | xargs -0 $COV -s $GITHUB_WORKSPACE/lib -pr + | xargs -0 $COV -s $GITHUB_WORKSPACE -pr popd $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ -Dsonar.host.url="$SONAR_SERVER_URL" \ -- cgit v1.2.3 From a207439d165b00b689e37b2759e0ca42bdfb22ae Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 08:00:39 +0100 Subject: Reinstate update-api jobs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27c65b26..5a3a0708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: qt-version: [ '5.12.10' ] # Not using binary values here, to make the job captions more readable e2ee: [ '' ] - update-api: [ '' ] #, 'update-api' ] + update-api: [ '', 'update-api' ] sonar: [ '' ] platform: [ '' ] qt-arch: [ '' ] -- cgit v1.2.3 From e4a9132ab5c684be9743823df7c31d12da1e3f3b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 10:40:44 +0100 Subject: CI: Add missing coverage files --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a3a0708..72deb803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,9 +217,11 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | mkdir .coverage && pushd .coverage - find $BUILD_PATH/CMakeFiles/Quotient.dir/lib -name '*.gcda' -print0 \ + find $BUILD_PATH -name '*.gcda' -print0 \ | xargs -0 $COV -s $GITHUB_WORKSPACE -pr popd + # Drop coverage of the test source code, which is obviously 100% + rm -f quotest* autotests* $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ -Dsonar.host.url="$SONAR_SERVER_URL" \ -Dsonar.cfamily.build-wrapper-output="$BUILD_PATH/sonar" \ -- cgit v1.2.3 From 179564eb4c2d5ee59ef129edab39ca1a7cbc4258 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 12:22:59 +0100 Subject: CI: Move a comment outside of the script Comments inside shell scripts apparently break the flimsy GHA machinery. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72deb803..5cac5874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,12 +215,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # Coverage of the test source code is not tracked, as it is obviously 100% + # (if it's not, some tests failed and break the build at an earlier stage) run: | mkdir .coverage && pushd .coverage find $BUILD_PATH -name '*.gcda' -print0 \ | xargs -0 $COV -s $GITHUB_WORKSPACE -pr popd - # Drop coverage of the test source code, which is obviously 100% rm -f quotest* autotests* $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ -Dsonar.host.url="$SONAR_SERVER_URL" \ -- cgit v1.2.3 From 165e17d4ad289dabdcb6a09ce27c02c6122b7f8a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 13:37:55 +0100 Subject: CI: Fix rm being run in the wrong directory It's been alright with the comment inside the script. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cac5874..a1b6f0c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,14 +215,14 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - # Coverage of the test source code is not tracked, as it is obviously 100% - # (if it's not, some tests failed and break the build at an earlier stage) run: | mkdir .coverage && pushd .coverage find $BUILD_PATH -name '*.gcda' -print0 \ | xargs -0 $COV -s $GITHUB_WORKSPACE -pr - popd + # Coverage of the test source code is not tracked, as it is always 100% + # (if not, some tests failed and broke the build at an earlier stage) rm -f quotest* autotests* + popd $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ -Dsonar.host.url="$SONAR_SERVER_URL" \ -Dsonar.cfamily.build-wrapper-output="$BUILD_PATH/sonar" \ -- cgit v1.2.3 From aeed7f3bada2cefcb52d4418b5ed76f19980d702 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 24 Dec 2021 14:29:07 +0100 Subject: Cache deviceslist to binary when possible --- lib/connection.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 8b9f9688..e28ffb22 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1982,9 +1982,16 @@ void Connection::Private::saveDevicesList() rootObj.insert(QStringLiteral("sync_token"), q->nextBatchToken()); } - - QJsonDocument json { rootObj }; - const auto data = json.toJson(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = + cacheToBinary + ? QCborValue::fromJsonValue(rootObj).toCbor() + : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); +#else + QJsonDocument json { rootObj }; + const auto data = cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); +#endif qCDebug(PROFILER) << "DeviceList generated in" << et; outFile.write(data.data(), data.size()); -- cgit v1.2.3 From ae0180acc50de443e98bc1c59dee94f0096df2e0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Dec 2021 15:40:13 +0100 Subject: Prune empty/ish call*event.cpp files --- CMakeLists.txt | 6 +++--- lib/events/callcandidatesevent.cpp | 27 --------------------------- lib/events/callhangupevent.cpp | 36 ------------------------------------ lib/events/callhangupevent.h | 8 ++++++-- 4 files changed, 9 insertions(+), 68 deletions(-) delete mode 100644 lib/events/callcandidatesevent.cpp delete mode 100644 lib/events/callhangupevent.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7675daad..adb5be7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,10 +162,10 @@ list(APPEND lib_SRCS lib/events/accountdataevents.h lib/events/receiptevent.h lib/events/receiptevent.cpp lib/events/reactionevent.h lib/events/reactionevent.cpp - lib/events/callanswerevent.h lib/events/callanswerevent.cpp - lib/events/callcandidatesevent.h lib/events/callcandidatesevent.cpp - lib/events/callhangupevent.h lib/events/callhangupevent.cpp lib/events/callinviteevent.h lib/events/callinviteevent.cpp + lib/events/callcandidatesevent.h + lib/events/callanswerevent.h lib/events/callanswerevent.cpp + lib/events/callhangupevent.h lib/events/directchatevent.h lib/events/directchatevent.cpp lib/events/encryptionevent.h lib/events/encryptionevent.cpp lib/events/encryptedevent.h lib/events/encryptedevent.cpp diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp deleted file mode 100644 index b87c8e9b..00000000 --- a/lib/events/callcandidatesevent.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "callcandidatesevent.h" - -/* -m.call.candidates -{ - "age": 242352, - "content": { - "call_id": "12345", - "candidates": [ - { - "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 -43670 typ host generation 0", "sdpMLineIndex": 0, "sdpMid": "audio" - } - ], - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.candidates" -} -*/ diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp deleted file mode 100644 index 43bc4db0..00000000 --- a/lib/events/callhangupevent.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard - * SPDX-FileCopyrightText: 2018 Josip Delic - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ - -#include "callhangupevent.h" - -/* -m.call.hangup -{ - "age": 242352, - "content": { - "call_id": "12345", - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.hangup" -} -*/ - -using namespace Quotient; - -CallHangupEvent::CallHangupEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Hangup event"; -} - -CallHangupEvent::CallHangupEvent(const QString& callId) - : CallEventBase(typeId(), matrixTypeId(), callId, 0) -{} diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index 24382ac2..f3f82833 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -11,8 +11,12 @@ class CallHangupEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - explicit CallHangupEvent(const QJsonObject& obj); - explicit CallHangupEvent(const QString& callId); + explicit CallHangupEvent(const QJsonObject& obj) + : CallEventBase(typeId(), obj) + {} + explicit CallHangupEvent(const QString& callId) + : CallEventBase(typeId(), matrixTypeId(), callId, 0) + {} }; REGISTER_EVENT_TYPE(CallHangupEvent) -- cgit v1.2.3 From 60947d610d0ece6943d2c2e385d6c6c2f960853d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 24 Dec 2021 17:07:29 +0100 Subject: Apply suggestions --- .github/workflows/sonar.yml | 121 --------------------------------------- CMakeLists.txt | 2 +- Makefile | 31 ---------- autotests/testolmaccount.cpp | 7 --- lib/converters.cpp | 2 +- lib/e2ee/qolmaccount.cpp | 2 +- lib/e2ee/qolmerrors.cpp | 13 +++-- lib/e2ee/qolmerrors.h | 2 +- lib/e2ee/qolminboundsession.cpp | 5 +- lib/e2ee/qolmoutboundsession.cpp | 3 +- lib/e2ee/qolmsession.cpp | 2 +- lib/e2ee/qolmutility.cpp | 2 +- run-tests.sh | 23 ++++++++ 13 files changed, 40 insertions(+), 175 deletions(-) delete mode 100644 .github/workflows/sonar.yml delete mode 100644 Makefile create mode 100755 run-tests.sh diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index c987b0cc..00000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: Sonar - -on: - push: - pull_request: - types: [opened, reopened] - -defaults: - run: - shell: bash - -jobs: - SonarCloud: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - qt-version: [ '5.12.10' ] - e2ee: [ '', 'e2ee' ] - update-api: [ '', 'update-api' ] - - env: - SONAR_SCANNER_VERSION: 4.6.2.2472 - SONAR_SERVER_URL: "https://sonarcloud.io" - BUILD_WRAPPER_OUT_DIR: build/sonar - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: ${{ matrix.e2ee != '' }} - - - name: Cache Qt - id: cache-qt - uses: actions/cache@v2 - with: - path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}-Qt${{ matrix.qt-version }}-cache - - - name: Install Qt - uses: jurplel/install-qt-action@v2.11.1 - with: - version: ${{ matrix.qt-version }} -# arch: ${{ matrix.qt-arch }} # Only Windows needs that - cached: ${{ steps.cache-qt.outputs.cache-hit }} - - - name: Install Ninja - uses: seanmiddleditch/gha-setup-ninja@v3 - - - name: Setup build environment - run: | - echo "CC=gcc-10" >>$GITHUB_ENV - echo "CXX=g++-10" >>$GITHUB_ENV - mkdir -p $HOME/.sonar - echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ - -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV - cmake -E make_directory ${{ runner.workspace }}/build - - - name: Build and install olm - if: matrix.e2ee - run: | - cd .. - git clone https://gitlab.matrix.org/matrix-org/olm.git - cmake -S olm -B olm/build $CMAKE_ARGS - cmake --build olm/build --target install - - - name: Build and install qtKeychain - if: matrix.e2ee - run: | - cd .. - git clone https://github.com/frankosterfeld/qtkeychain.git - cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS - cmake --build qtkeychain/build --target install - - - name: Pull CS API and build GTAD - if: matrix.update-api - run: | - cd .. - git clone https://github.com/quotient-im/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF - cmake --build gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ - -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ - >>$GITHUB_ENV - - - name: Download and set up Sonar Cloud tools - run: | - pushd $HOME/.sonar - curl -sSLo build-wrapper.zip $SONAR_SERVER_URL/static/cpp/build-wrapper-linux-x86.zip - unzip -o build-wrapper.zip - echo "BUILD_WRAPPER=$HOME/.sonar/build-wrapper-linux-x86/build-wrapper-linux* --out-dir $BUILD_WRAPPER_OUT_DIR" >>$GITHUB_ENV - curl -sSLo sonar-scanner.zip \ - https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip - unzip -o sonar-scanner.zip - popd - - - name: Configure libQuotient - run: | - if [[ '${{ runner.os }}' == 'Windows' ]]; then - BIN_DIR=. - else - BIN_DIR=bin - fi - echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV - cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} - - - name: Regenerate API code - if: matrix.update-api - run: cmake --build build --target update-api - - - name: Build libQuotient - run: | - $BUILD_WRAPPER cmake --build build --target all - - - name: Run sonar-scanner - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - $HOME/.sonar/sonar-scanner*/bin/sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ff65282..9ef3477e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,7 +89,7 @@ message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") if (${PROJECT_NAME}_ENABLE_E2EE) find_package(${Qt} ${QtMinVersion} REQUIRED Sql) - find_package(Olm 3.2.1 REQUIRED) + find_package(Olm 3.1.3 REQUIRED) set_package_properties(Olm PROPERTIES DESCRIPTION "Implementation of the Olm and Megolm cryptographic ratchets" URL "https://gitlab.matrix.org/matrix-org/olm" diff --git a/Makefile b/Makefile deleted file mode 100644 index 450e7888..00000000 --- a/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -SYNAPSE_IMAGE="matrixdotorg/synapse:v1.24.0" - -test: ## Run the tests - @cd build/ && GTEST_COLOR=1 ctest --verbose - -synapse: ## Start a synapse instance on docker - @mkdir -p data - @chmod 0777 data - @docker run -v `pwd`/data:/data --rm \ - -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no ${SYNAPSE_IMAGE} generate - @./.ci/adjust-config.sh - @docker run -d \ - --name synapse \ - -p 443:8008 \ - -p 8448:8008 \ - -p 8008:8008 \ - -v `pwd`/data:/data ${SYNAPSE_IMAGE} - @echo Waiting for synapse to start... - @until curl -s -f -k https://localhost:443/_matrix/client/versions; do echo "Checking ..."; sleep 2; done - @echo Register alice - @docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice -p secret -c /data/homeserver.yaml https://localhost:8008' - @echo Register bob - @docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -p secret -c /data/homeserver.yaml https://localhost:8008' - @echo Register carl - @docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' - -stop-synapse: ## Stop any running instance of synapse - @rm -rf ./data/* - @docker rm -f synapse 2>&1>/dev/null - -restart: stop-synapse synapse diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index f0fcfe58..d547b683 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -12,13 +12,6 @@ #include #include -// for sleep -#ifdef _WIN32 -#include -#else -#include -#endif - using namespace Quotient; void TestOlmAccount::pickleUnpickledTest() diff --git a/lib/converters.cpp b/lib/converters.cpp index 4136940f..6cbb554d 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -3,7 +3,7 @@ #include "converters.h" -#include +#include #include "e2ee/e2ee.h" QJsonValue Quotient::JsonConverter::dump(const QVariant& v) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index aaf51946..ffb004cc 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -34,7 +34,7 @@ bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) // Convert olm error to enum QOlmError lastError(OlmAccount *account) { - const std::string error_raw = olm_account_last_error(account); + const auto error_raw = olm_account_last_error(account); return fromString(error_raw); } diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp index 6db1803c..568cf7fe 100644 --- a/lib/e2ee/qolmerrors.cpp +++ b/lib/e2ee/qolmerrors.cpp @@ -4,17 +4,18 @@ #include "qolmerrors.h" +#include -Quotient::QOlmError Quotient::fromString(const std::string &error_raw) { - if (!error_raw.compare("BAD_ACCOUNT_KEY")) { +Quotient::QOlmError Quotient::fromString(const char* error_raw) { + if (!strncmp(error_raw, "BAD_ACCOUNT_KEY", 15)) { return QOlmError::BadAccountKey; - } else if (!error_raw.compare("BAD_MESSAGE_KEY_ID")) { + } else if (!strncmp(error_raw, "BAD_MESSAGE_KEY_ID", 18)) { return QOlmError::BadMessageKeyId; - } else if (!error_raw.compare("INVALID_BASE64")) { + } else if (!strncmp(error_raw, "INVALID_BASE64", 14)) { return QOlmError::InvalidBase64; - } else if (!error_raw.compare("NOT_ENOUGH_RANDOM")) { + } else if (!strncmp(error_raw, "NOT_ENOUGH_RANDOM", 17)) { return QOlmError::NotEnoughRandom; - } else if (!error_raw.compare("OUTPUT_BUFFER_TOO_SMALL")) { + } else if (!strncmp(error_raw, "OUTPUT_BUFFER_TOO_SMALL", 23)) { return QOlmError::OutputBufferTooSmall; } else { return QOlmError::Unknown; diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h index f8390d2a..f2d77851 100644 --- a/lib/e2ee/qolmerrors.h +++ b/lib/e2ee/qolmerrors.h @@ -23,6 +23,6 @@ enum QOlmError Unknown, }; -QOlmError fromString(const std::string &error_raw); +QOlmError fromString(const char* error_raw); } //namespace Quotient diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 9bf56b6c..2c546875 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -8,7 +8,7 @@ using namespace Quotient; QOlmError lastError(OlmInboundGroupSession *session) { - const std::string error_raw = olm_inbound_group_session_last_error(session); + const auto error_raw = olm_inbound_group_session_last_error(session); return fromString(error_raw); } @@ -27,9 +27,8 @@ QOlmInboundGroupSession::~QOlmInboundGroupSession() std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - const auto temp = key; const auto error = olm_init_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(temp.data()), temp.size()); + reinterpret_cast(key.constData()), key.size()); if (error == olm_error()) { throw lastError(olmInboundGroupSession); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 88e6b2e1..58196412 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -8,7 +8,7 @@ using namespace Quotient; QOlmError lastError(OlmOutboundGroupSession *session) { - const std::string error_raw = olm_outbound_group_session_last_error(session); + const auto error_raw = olm_outbound_group_session_last_error(session); return fromString(error_raw); } @@ -21,6 +21,7 @@ QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *sess QOlmOutboundGroupSession::~QOlmOutboundGroupSession() { olm_clear_outbound_group_session(m_groupSession); + Q_ASSERT(sizeof(m_groupSession) == olm_outbound_group_session_size()); delete[](reinterpret_cast(m_groupSession)); } diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 69d8b431..575019b3 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -11,7 +11,7 @@ using namespace Quotient; QOlmError lastError(OlmSession* session) { - const std::string error_raw = olm_session_last_error(session); + const auto error_raw = olm_session_last_error(session); return fromString(error_raw); } diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index d0684055..13ee695e 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -10,7 +10,7 @@ using namespace Quotient; // Convert olm error to enum QOlmError lastError(OlmUtility *utility) { - const std::string error_raw = olm_utility_last_error(utility); + const auto error_raw = olm_utility_last_error(utility); return fromString(error_raw); } diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 00000000..b49f37a1 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,23 @@ +mkdir -p data +chmod 0777 data +docker run -v `pwd`/data:/data --rm \ + -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:v1.24.0 generate +./.ci/adjust-config.sh +docker run -d \ + --name synapse \ + -p 1234:8008 \ + -p 8448:8008 \ + -p 8008:8008 \ + -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0 +echo Waiting for synapse to start... +until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done +echo Register alice +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice -p secret -c /data/homeserver.yaml https://localhost:8008' +echo Register bob +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -p secret -c /data/homeserver.yaml https://localhost:8008' +echo Register carl +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' + +cd build/ && GTEST_COLOR=1 ctest --verbose +rm -rf ./data/* +docker rm -f synapse 2>&1>/dev/null -- cgit v1.2.3 From f01427c307c825feaa6c45888c259903605c9796 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 25 Dec 2021 15:59:03 +0100 Subject: Update lib/e2ee/qolmoutboundsession.cpp Co-authored-by: Alexey Rusakov --- lib/e2ee/qolmoutboundsession.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 58196412..8494efdd 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -64,7 +64,6 @@ std::variant QOlmOutboundGroupSession::pickle(const Pickl return pickledBuf; } - std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; -- cgit v1.2.3 From f4d6a08811b0bf3a10a23ff703fc5a8ff1fcc624 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 Dec 2021 16:00:09 +0100 Subject: Apply suggestions --- lib/e2ee/qolmaccount.cpp | 4 +--- lib/e2ee/qolmerrors.cpp | 14 ++++++++------ lib/e2ee/qolmerrors.h | 2 -- lib/e2ee/qolminboundsession.cpp | 4 +--- lib/e2ee/qolmoutboundsession.cpp | 4 +--- lib/e2ee/qolmsession.cpp | 4 +--- lib/e2ee/qolmutility.cpp | 4 +--- 7 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index ffb004cc..a984f884 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -34,9 +34,7 @@ bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) // Convert olm error to enum QOlmError lastError(OlmAccount *account) { - const auto error_raw = olm_account_last_error(account); - - return fromString(error_raw); + return fromString(olm_account_last_error(account)); } QByteArray getRandom(size_t bufferSize) diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp index 568cf7fe..5a60b7e6 100644 --- a/lib/e2ee/qolmerrors.cpp +++ b/lib/e2ee/qolmerrors.cpp @@ -4,18 +4,20 @@ #include "qolmerrors.h" -#include +#include "util.h" +#include Quotient::QOlmError Quotient::fromString(const char* error_raw) { - if (!strncmp(error_raw, "BAD_ACCOUNT_KEY", 15)) { + const QLatin1String error { error_raw }; + if (error_raw == "BAD_ACCOUNT_KEY"_ls) { return QOlmError::BadAccountKey; - } else if (!strncmp(error_raw, "BAD_MESSAGE_KEY_ID", 18)) { + } else if (error_raw == "BAD_MESSAGE_KEY_ID"_ls) { return QOlmError::BadMessageKeyId; - } else if (!strncmp(error_raw, "INVALID_BASE64", 14)) { + } else if (error_raw == "INVALID_BASE64"_ls) { return QOlmError::InvalidBase64; - } else if (!strncmp(error_raw, "NOT_ENOUGH_RANDOM", 17)) { + } else if (error_raw == "NOT_ENOUGH_RANDOM"_ls) { return QOlmError::NotEnoughRandom; - } else if (!strncmp(error_raw, "OUTPUT_BUFFER_TOO_SMALL", 23)) { + } else if (error_raw == "OUTPUT_BUFFER_TOO_SMALL"_ls) { return QOlmError::OutputBufferTooSmall; } else { return QOlmError::Unknown; diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h index f2d77851..24e87d95 100644 --- a/lib/e2ee/qolmerrors.h +++ b/lib/e2ee/qolmerrors.h @@ -4,8 +4,6 @@ #pragma once -#include - namespace Quotient { //! All errors that could be caused by an operation regarding Olm //! Errors are named exactly like the ones in libolm. diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 2c546875..9729c02d 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -8,9 +8,7 @@ using namespace Quotient; QOlmError lastError(OlmInboundGroupSession *session) { - const auto error_raw = olm_inbound_group_session_last_error(session); - - return fromString(error_raw); + return fromString(olm_inbound_group_session_last_error(session)); } QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 8494efdd..e75ab427 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -8,9 +8,7 @@ using namespace Quotient; QOlmError lastError(OlmOutboundGroupSession *session) { - const auto error_raw = olm_outbound_group_session_last_error(session); - - return fromString(error_raw); + return fromString(olm_outbound_group_session_last_error(session)); } QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 575019b3..e575ff39 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -11,9 +11,7 @@ using namespace Quotient; QOlmError lastError(OlmSession* session) { - const auto error_raw = olm_session_last_error(session); - - return fromString(error_raw); + return fromString(olm_session_last_error(session)); } Quotient::QOlmSession::~QOlmSession() diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 13ee695e..303f6d75 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -10,9 +10,7 @@ using namespace Quotient; // Convert olm error to enum QOlmError lastError(OlmUtility *utility) { - const auto error_raw = olm_utility_last_error(utility); - - return fromString(error_raw); + return fromString(olm_utility_last_error(utility)); } QOlmUtility::QOlmUtility() -- cgit v1.2.3 From 43470ab005512acb5f137aa6b9026ebcf3fcc142 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 Dec 2021 16:01:32 +0100 Subject: Move run-tests.sh --- autotests/run-tests.sh | 23 +++++++++++++++++++++++ run-tests.sh | 23 ----------------------- 2 files changed, 23 insertions(+), 23 deletions(-) create mode 100755 autotests/run-tests.sh delete mode 100755 run-tests.sh diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh new file mode 100755 index 00000000..b49f37a1 --- /dev/null +++ b/autotests/run-tests.sh @@ -0,0 +1,23 @@ +mkdir -p data +chmod 0777 data +docker run -v `pwd`/data:/data --rm \ + -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:v1.24.0 generate +./.ci/adjust-config.sh +docker run -d \ + --name synapse \ + -p 1234:8008 \ + -p 8448:8008 \ + -p 8008:8008 \ + -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0 +echo Waiting for synapse to start... +until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done +echo Register alice +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice -p secret -c /data/homeserver.yaml https://localhost:8008' +echo Register bob +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -p secret -c /data/homeserver.yaml https://localhost:8008' +echo Register carl +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' + +cd build/ && GTEST_COLOR=1 ctest --verbose +rm -rf ./data/* +docker rm -f synapse 2>&1>/dev/null diff --git a/run-tests.sh b/run-tests.sh deleted file mode 100755 index b49f37a1..00000000 --- a/run-tests.sh +++ /dev/null @@ -1,23 +0,0 @@ -mkdir -p data -chmod 0777 data -docker run -v `pwd`/data:/data --rm \ - -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:v1.24.0 generate -./.ci/adjust-config.sh -docker run -d \ - --name synapse \ - -p 1234:8008 \ - -p 8448:8008 \ - -p 8008:8008 \ - -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0 -echo Waiting for synapse to start... -until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done -echo Register alice -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice -p secret -c /data/homeserver.yaml https://localhost:8008' -echo Register bob -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -p secret -c /data/homeserver.yaml https://localhost:8008' -echo Register carl -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' - -cd build/ && GTEST_COLOR=1 ctest --verbose -rm -rf ./data/* -docker rm -f synapse 2>&1>/dev/null -- cgit v1.2.3 From 5af76cb4bedaea85bf8e2a8538fabe4e47d40e1a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 Dec 2021 16:56:17 +0100 Subject: Remove unneeded code --- lib/converters.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/converters.cpp b/lib/converters.cpp index 6cbb554d..b80f5985 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -8,21 +8,11 @@ QJsonValue Quotient::JsonConverter::dump(const QVariant& v) { - if (v.canConvert()) { - return toJson(v.value()); - } return QJsonValue::fromVariant(v); } QVariant Quotient::JsonConverter::load(const QJsonValue& jv) { - if (jv.isObject()) { - const QJsonObject obj = jv.toObject(); - if (obj.contains(QLatin1String("key")) && obj.contains(QLatin1String("signatures"))) { - SignedOneTimeKey signedOneTimeKeys; - signedOneTimeKeys.key = obj[QLatin1String("key")].toString(); - } - } return jv.toVariant(); } -- cgit v1.2.3 From 586332cda3d786ccbe74b21bd70d290fee722719 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 Dec 2021 17:03:21 +0100 Subject: Remove irrelevant include --- lib/converters.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/converters.cpp b/lib/converters.cpp index b80f5985..444ca4f6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -4,7 +4,6 @@ #include "converters.h" #include -#include "e2ee/e2ee.h" QJsonValue Quotient::JsonConverter::dump(const QVariant& v) { -- cgit v1.2.3 From d56bb69f06c70bb7cf659d5fdde4e2306d7fb2f2 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 Dec 2021 18:19:46 +0100 Subject: =?UTF-8?q?Don't=20save=20olm=20account=20=C3=B3n=20shutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It causes the program to crash for some reason --- lib/connection.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index e28ffb22..138f968f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -269,9 +269,7 @@ Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent), d(new Private(std::make_unique(server))) { #ifdef Quotient_E2EE_ENABLED - connect(qApp, &QCoreApplication::aboutToQuit, this, [this](){ - saveOlmAccount(); - }); + //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount); #endif d->q = this; // All d initialization should occur before this line } -- cgit v1.2.3 From 9ef0c672595f47532966566ec08f16424fcf1abb Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 25 Dec 2021 18:22:25 +0100 Subject: Update lib/e2ee/qolmoutboundsession.h Co-authored-by: Alexey Rusakov --- lib/e2ee/qolmoutboundsession.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 967f563f..72d87035 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -12,7 +12,6 @@ namespace Quotient { - //! An out-bound group session is responsible for encrypting outgoing //! communication in a Megolm session. class QOlmOutboundGroupSession -- cgit v1.2.3 From b465e785aac47bab8f4b8f4dac9672c9c32ea020 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 25 Dec 2021 18:22:31 +0100 Subject: Update lib/e2ee/qolmoutboundsession.h Co-authored-by: Alexey Rusakov --- lib/e2ee/qolmoutboundsession.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 72d87035..39263c77 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later - #pragma once #include "olm/olm.h" -- cgit v1.2.3 From 3164daa6d6cbfb75a1528b2671f5c78422a5539e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 Dec 2021 19:57:26 +0100 Subject: Remove assert --- lib/e2ee/qolmoutboundsession.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index e75ab427..da32417b 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -19,7 +19,6 @@ QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *sess QOlmOutboundGroupSession::~QOlmOutboundGroupSession() { olm_clear_outbound_group_session(m_groupSession); - Q_ASSERT(sizeof(m_groupSession) == olm_outbound_group_session_size()); delete[](reinterpret_cast(m_groupSession)); } -- cgit v1.2.3 From ff415143b0d953f04a207125e265111b925ce763 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 25 Dec 2021 21:51:17 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/connection.cpp | 6 +++--- lib/csapi/keys.h | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 138f968f..89b80909 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -112,7 +112,7 @@ public: #ifdef Quotient_E2EE_ENABLED QSet trackedUsers; QSet outdatedUsers; - QHash> deviceKeys; + QHash> deviceKeys; QueryKeysJob *currentQueryKeysJob = nullptr; bool encryptionUpdateRequired = false; PicklingMode picklingMode = Unencrypted {}; @@ -1975,7 +1975,7 @@ void Connection::Private::saveDevicesList() } rootObj.insert(QStringLiteral("tracked_users"), trackedUsersJson); rootObj.insert(QStringLiteral("outdated_users"), outdatedUsersJson); - QJsonObject devicesList = toJson>>(deviceKeys); + const auto devicesList = toJson(deviceKeys); rootObj.insert(QStringLiteral("devices_list"), devicesList); rootObj.insert(QStringLiteral("sync_token"), q->nextBatchToken()); } @@ -2023,7 +2023,7 @@ void Connection::Private::loadDevicesList() outdatedUsers += user.toString(); } - deviceKeys = fromJson>>(json["devices_list"].toObject()); + fromJson(json["devices_list"], deviceKeys); auto oldToken = json["sync_token"].toString(); auto changesJob = q->callApi(oldToken, q->nextBatchToken()); connect(changesJob, &BaseJob::success, q, [this, changesJob](){ diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index b1cc640c..7db09e8d 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -166,11 +166,6 @@ struct JsonObjectConverter { fillFromJson(jo, result); fromJson(jo.value("unsigned"_ls), result.unsignedData); } - static void dumpTo(QJsonObject& jo, const QueryKeysJob::DeviceInformation& deviceInformation) - { - jo = toJson(deviceInformation); - //addParam<>(jo, "unsigned"_ls, deviceInformation.unsignedData); - } }; /*! \brief Claim one-time encryption keys. -- cgit v1.2.3 From ebff6633ad464991fdffed1841ec6dc5b34296a2 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 25 Dec 2021 23:46:14 +0100 Subject: Make canChangePassword available from QML --- lib/connection.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connection.h b/lib/connection.h index 05a3bb7f..52bf3cea 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -124,6 +124,7 @@ class Connection : public QObject { Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged STORED false) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded) public: using UsersToDevicesToEvents = -- cgit v1.2.3 From 53c494f1b9f273395caade35c93e3fb520d083ec Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 27 Dec 2021 20:15:47 +0100 Subject: Quotest: add compile warnings from libQuotient --- quotest/CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index 8d67b6f1..cb41141d 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -8,6 +8,21 @@ find_package(${Qt} COMPONENTS Concurrent) add_executable(quotest ${quotest_SRCS}) target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME}) +if (MSVC) + target_compile_options(quotest PUBLIC /EHsc /W4 + /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 + /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 + /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) +else() + foreach (FLAG W Wall Wpedantic Wextra Wno-unused-parameter Werror=return-type) + CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) + if (COMPILER_${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") + target_compile_options(quotest PUBLIC -${FLAG}) + endif () + endforeach () +endif() + option(${PROJECT_NAME}_INSTALL_TESTS "install quotest application" ON) add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS "the library functional test suite") -- cgit v1.2.3 From 58b2501aeecf2c169fd1f583dca292169568b5fa Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 26 Dec 2021 09:29:00 +0100 Subject: Connection: Simplify room/user factory code There's no need to return lambdas where pointers to specialised function templates would work just fine. --- lib/connection.cpp | 4 ++-- lib/connection.h | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 25219def..8d1c80f1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1513,8 +1513,8 @@ room_factory_t Connection::roomFactory() { return _roomFactory; } user_factory_t Connection::userFactory() { return _userFactory; } -room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); -user_factory_t Connection::_userFactory = defaultUserFactory<>(); +room_factory_t Connection::_roomFactory = defaultRoomFactory<>; +user_factory_t Connection::_userFactory = defaultUserFactory<>; QByteArray Connection::generateTxnId() const { diff --git a/lib/connection.h b/lib/connection.h index 52bf3cea..0713af16 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -81,11 +81,9 @@ using user_factory_t = std::function; * \sa Connection::setRoomFactory, Connection::setRoomType */ template -static inline room_factory_t defaultRoomFactory() +auto defaultRoomFactory(Connection* c, const QString& id, JoinState js) { - return [](Connection* c, const QString& id, JoinState js) { - return new T(c, id, js); - }; + return new T(c, id, js); } /** The default factory to create user objects @@ -94,9 +92,9 @@ static inline room_factory_t defaultRoomFactory() * \sa Connection::setUserFactory, Connection::setUserType */ template -static inline user_factory_t defaultUserFactory() +auto defaultUserFactory(Connection* c, const QString& id) { - return [](Connection* c, const QString& id) { return new T(id, c); }; + return new T(id, c); } // Room ids, rather than room pointers, are used in the direct chat @@ -469,14 +467,14 @@ public: template static void setRoomType() { - setRoomFactory(defaultRoomFactory()); + setRoomFactory(defaultRoomFactory); } /// Set the user factory to default with the overriden user type template static void setUserType() { - setUserFactory(defaultUserFactory()); + setUserFactory(defaultUserFactory); } public Q_SLOTS: -- cgit v1.2.3 From 674e984e459375974f619d0e778d43a2cc928dc3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 26 Dec 2021 09:33:14 +0100 Subject: Key* strings: drop 'static'; add 'constexpr' where ok --- lib/events/event.h | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index e786fb30..8f62872d 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -29,24 +29,24 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt& ptr) // === Standard Matrix key names and basicEventJson() === -static const auto TypeKey = QStringLiteral("type"); -static const auto BodyKey = QStringLiteral("body"); -static const auto ContentKey = QStringLiteral("content"); -static const auto EventIdKey = QStringLiteral("event_id"); -static const auto SenderKey = QStringLiteral("sender"); -static const auto RoomIdKey = QStringLiteral("room_id"); -static const auto UnsignedKey = QStringLiteral("unsigned"); -static const auto StateKeyKey = QStringLiteral("state_key"); -static const auto TypeKeyL = "type"_ls; -static const auto BodyKeyL = "body"_ls; -static const auto ContentKeyL = "content"_ls; -static const auto EventIdKeyL = "event_id"_ls; -static const auto SenderKeyL = "sender"_ls; -static const auto RoomIdKeyL = "room_id"_ls; -static const auto UnsignedKeyL = "unsigned"_ls; -static const auto RedactedCauseKeyL = "redacted_because"_ls; -static const auto PrevContentKeyL = "prev_content"_ls; -static const auto StateKeyKeyL = "state_key"_ls; +constexpr auto TypeKeyL = "type"_ls; +constexpr auto BodyKeyL = "body"_ls; +constexpr auto ContentKeyL = "content"_ls; +constexpr auto EventIdKeyL = "event_id"_ls; +constexpr auto SenderKeyL = "sender"_ls; +constexpr auto RoomIdKeyL = "room_id"_ls; +constexpr auto UnsignedKeyL = "unsigned"_ls; +constexpr auto RedactedCauseKeyL = "redacted_because"_ls; +constexpr auto PrevContentKeyL = "prev_content"_ls; +constexpr auto StateKeyKeyL = "state_key"_ls; +const QString TypeKey { TypeKeyL }; +const QString BodyKey { BodyKeyL }; +const QString ContentKey { ContentKeyL }; +const QString EventIdKey { EventIdKeyL }; +const QString SenderKey { SenderKeyL }; +const QString RoomIdKey { RoomIdKeyL }; +const QString UnsignedKey { UnsignedKeyL }; +const QString StateKeyKey { StateKeyKeyL }; /// Make a minimal correct Matrix event JSON inline QJsonObject basicEventJson(const QString& matrixType, -- cgit v1.2.3 From 3e0ca5db7698d59c6f2e9e7ae3a3a1654641eb4e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 26 Dec 2021 10:38:49 +0100 Subject: EventItem::setUserData: use std::move Fixes a clang-tidy warning. --- lib/eventitem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eventitem.h b/lib/eventitem.h index 0ab1a01d..b411a90c 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -57,7 +57,7 @@ public: } /// Store arbitrary data with the event item - void setUserData(std::any userData) { data = userData; } + void setUserData(std::any userData) { data = std::move(userData); } /// Obtain custom data previously stored with the event item const std::any& userdata() const { return data; } std::any& userData() { return data; } -- cgit v1.2.3 From 5f4d9ef6df492bfd91fa7c48fb4d5f8c2afc40b2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 27 Dec 2021 20:14:07 +0100 Subject: EventFactory: remove default constructor This is a leftover from deferred `name` initialisation that wasn't needed in the end. --- lib/events/event.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/event.h b/lib/events/event.h index 8f62872d..8347bb4f 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -146,7 +146,7 @@ namespace _impl { // Actual makeIfMatches specialisations will differ in the first // template parameter but that doesn't affect the function type public: - explicit EventFactory(const char* name = "") + explicit EventFactory(const char* name) : name(name) { static auto yetToBeConstructed = true; -- cgit v1.2.3 From 7bb59b7a4a8b8804fa1eb5e631463da6a6cfdf84 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Dec 2021 08:33:56 +0100 Subject: Convert struct LoginFlows to namespace Because that's what it really is. --- lib/connection.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 0713af16..45abb8ed 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -51,11 +51,11 @@ class LeaveRoomJob; using LoginFlow = GetLoginFlowsJob::LoginFlow; /// Predefined login flows -struct LoginFlows { - static inline const LoginFlow Password { "m.login.password" }; - static inline const LoginFlow SSO { "m.login.sso" }; - static inline const LoginFlow Token { "m.login.token" }; -}; +namespace LoginFlows { + inline const LoginFlow Password { "m.login.password" }; + inline const LoginFlow SSO { "m.login.sso" }; + inline const LoginFlow Token { "m.login.token" }; +} // To simplify comparisons of LoginFlows -- cgit v1.2.3 From 0d9428bfbd997ac2073a470e59749cda81e0b213 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Dec 2021 09:01:37 +0100 Subject: Settings classes: unify on QUO_ macro prefix At some point macros were prepended with QTNT (pronounced "cute-n't", "Quotient" with vowels dropped) but that didn't go very far. Having forgotten about this, I introduced QUO prefix in a few places. Being initial letters of "Quotient", QUO feels more understandable (and coincidentally is a well-known Latin word); so let's unify on this. --- lib/networksettings.cpp | 6 +++--- lib/networksettings.h | 6 +++--- lib/settings.cpp | 6 +++--- lib/settings.h | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index ce46ce5f..06b1fdf9 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -11,9 +11,9 @@ void NetworkSettings::setupApplicationProxy() const { proxyType(), proxyHostName(), proxyPort() }); } -QTNT_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, +QUO_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QTNT_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", +QUO_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) -QTNT_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, +QUO_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/networksettings.h b/lib/networksettings.h index df11a9c8..b83a0e15 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -12,9 +12,9 @@ Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) namespace Quotient { class NetworkSettings : public SettingsGroup { Q_OBJECT - QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) - QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) - QTNT_DECLARE_SETTING(quint16, proxyPort, setProxyPort) + QUO_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) + QUO_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) + QUO_DECLARE_SETTING(quint16, proxyPort, setProxyPort) Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) public: template diff --git a/lib/settings.cpp b/lib/settings.cpp index ed9082b0..5549e4de 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -102,11 +102,11 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QTNT_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, +QUO_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) -QTNT_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, +QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) -QTNT_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, +QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) static const auto HomeserverKey = QStringLiteral("homeserver"); diff --git a/lib/settings.h b/lib/settings.h index efd0d714..3cab8958 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -104,7 +104,7 @@ private: QString groupPath; }; -#define QTNT_DECLARE_SETTING(type, propname, setter) \ +#define QUO_DECLARE_SETTING(type, propname, setter) \ Q_PROPERTY(type propname READ propname WRITE setter) \ public: \ type propname() const; \ @@ -112,7 +112,7 @@ public: \ \ private: -#define QTNT_DEFINE_SETTING(classname, type, propname, qsettingname, \ +#define QUO_DEFINE_SETTING(classname, type, propname, qsettingname, \ defaultValue, setter) \ type classname::propname() const \ { \ @@ -127,9 +127,9 @@ private: class AccountSettings : public SettingsGroup { Q_OBJECT Q_PROPERTY(QString userId READ userId CONSTANT) - QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) - QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName) - QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) + QUO_DECLARE_SETTING(QString, deviceId, setDeviceId) + QUO_DECLARE_SETTING(QString, deviceName, setDeviceName) + QUO_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle WRITE setEncryptionAccountPickle) public: -- cgit v1.2.3 From 418b85c285fa0a0c196a26eef5cc0c9c3dbe20fe Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Dec 2021 09:07:37 +0100 Subject: EventContent::FileInfo: default payloadSize to 0 Fixes a clang-tidy warning. --- lib/events/eventcontent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index f609a603..f6dbd4bf 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -105,7 +105,7 @@ namespace EventContent { QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; - qint64 payloadSize; + qint64 payloadSize = 0; QString originalName; Omittable file = none; }; -- cgit v1.2.3 From 474d71067462f821697111a727e5f56767d2b00e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 29 Dec 2021 18:07:08 +0100 Subject: Introduce quotient_export.h Instead of using CMake's generate_export_header macro, it's a bit easier to maintain a static file (that is not supposed to ever change) with necessary export/import/hidden visibility macros. --- CMakeLists.txt | 1 + lib/quotient_export.h | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 lib/quotient_export.h diff --git a/CMakeLists.txt b/CMakeLists.txt index adb5be7b..0fc47b42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,7 @@ endif () # Set up source files list(APPEND lib_SRCS lib/quotient_common.h + lib/quotient_export.h lib/function_traits.h lib/function_traits.cpp lib/networkaccessmanager.h lib/networkaccessmanager.cpp lib/connectiondata.h lib/connectiondata.cpp diff --git a/lib/quotient_export.h b/lib/quotient_export.h new file mode 100644 index 00000000..5a6edb0e --- /dev/null +++ b/lib/quotient_export.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef QUOTIENT_STATIC +# define QUOTIENT_API +# define QUOTIENT_HIDDEN +#else +# ifndef QUOTIENT_API +# ifdef BUILDING_SHARED_QUOTIENT + /* We are building this library */ +# define QUOTIENT_API Q_DECL_EXPORT +# else + /* We are using this library */ +# define QUOTIENT_API Q_DECL_IMPORT +# endif +# endif + +# ifndef QUOTIENT_HIDDEN +# define QUOTIENT_HIDDEN Q_DECL_HIDDEN +# endif +#endif -- cgit v1.2.3 From 02fdd08b98d878168eb81376a44586176dfd9576 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 26 Dec 2021 10:40:56 +0100 Subject: Quotest: test sending and receiving custom events This seems to be the crux of #413. --- quotest/quotest.cpp | 57 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 7bd9c5c3..5a5642fe 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -101,6 +101,7 @@ private slots: TEST_DECL(sendMessage) TEST_DECL(sendReaction) TEST_DECL(sendFile) + TEST_DECL(sendCustomEvent) TEST_DECL(setTopic) TEST_DECL(changeName) TEST_DECL(sendAndRedact) @@ -125,6 +126,7 @@ private: [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, const QString& evtIdToRedact); + template [[nodiscard]] bool validatePendingEvent(const QString& txnId); [[nodiscard]] bool checkDirectChat() const; void finishTest(const TestToken& token, bool condition, const char* file, @@ -152,12 +154,14 @@ void TestSuite::doTest(const QByteArray& testName) Q_ARG(TestToken, testName)); } +template bool TestSuite::validatePendingEvent(const QString& txnId) { auto it = targetRoom->findPendingEvent(txnId); return it != targetRoom->pendingEvents().end() && it->deliveryStatus() == EventStatus::Submitted - && (*it)->transactionId() == txnId; + && (*it)->transactionId() == txnId && is(**it) + && (*it)->matrixType() == EventT::matrixTypeId(); } void TestSuite::finishTest(const TestToken& token, bool condition, @@ -341,7 +345,7 @@ TEST_IMPL(loadMembers) TEST_IMPL(sendMessage) { auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - if (!validatePendingEvent(txnId)) { + if (!validatePendingEvent(txnId)) { clog << "Invalid pending event right after submitting" << endl; FAIL_TEST(); } @@ -367,7 +371,7 @@ TEST_IMPL(sendReaction) const auto targetEvtId = targetRoom->messageEvents().back()->id(); const auto key = QStringLiteral("+1"); const auto txnId = targetRoom->postReaction(targetEvtId, key); - if (!validatePendingEvent(txnId)) { + if (!validatePendingEvent(txnId)) { clog << "Invalid pending event right after submitting" << endl; FAIL_TEST(); } @@ -409,7 +413,7 @@ TEST_IMPL(sendFile) clog << "Sending file " << tfName.toStdString() << endl; const auto txnId = targetRoom->postFile( "Test file", new EventContent::FileContent(tfi)); - if (!validatePendingEvent(txnId)) { + if (!validatePendingEvent(txnId)) { clog << "Invalid pending event right after submitting" << endl; tf->deleteLater(); FAIL_TEST(); @@ -518,6 +522,50 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return true; } +class CustomEvent : public RoomEvent { +public: + DEFINE_EVENT_TYPEID("quotest.custom", CustomEvent) + + CustomEvent(const QJsonObject& jo) + : RoomEvent(typeId(), jo) + {} + CustomEvent(int testValue) + : RoomEvent(typeId(), + basicEventJson(matrixTypeId(), + QJsonObject { { "testValue"_ls, + toJson(testValue) } })) + {} + + auto testValue() const { return contentPart("testValue"_ls); } +}; +REGISTER_EVENT_TYPE(CustomEvent) + +TEST_IMPL(sendCustomEvent) +{ + auto txnId = targetRoom->postEvent(new CustomEvent(42)); + if (!validatePendingEvent(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + connectUntil( + targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId](const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + return switchOnType(*evt, + [this, thisTest, &evt](const CustomEvent& e) { + FINISH_TEST(!evt->id().isEmpty() && e.testValue() == 42); + }, + [this, thisTest] (const RoomEvent&) { FAIL_TEST(); }); + }); + return false; + +} + TEST_IMPL(setTopic) { const auto newTopic = connection()->generateTxnId(); // Just a way to make @@ -567,7 +615,6 @@ TEST_IMPL(changeName) return false; } - TEST_IMPL(showLocalUsername) { auto* const localUser = connection()->user(); -- cgit v1.2.3 From 7350fe82953cf6274b8845a890eafb21a09b9931 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 29 Dec 2021 15:59:58 +0100 Subject: Add QUOTIENT_API throughout non-generated code This include all (hopefully) classes/structures and functions that have non-inline definitions, as well as namespaces with Q_NAMESPACE since those have non-inline (as of Qt 5.15) QMetaObject - for that a new macro, QUO_NAMESPACE, has been devised to accommodate the lack of Q_NAMESPACE_EXPORT in Qt before 5.14. --- lib/accountregistry.h | 4 +++- lib/avatar.h | 4 +++- lib/connection.h | 2 +- lib/converters.h | 6 +++--- lib/eventitem.cpp | 4 ++++ lib/eventitem.h | 9 +++++---- lib/events/accountdataevents.h | 2 +- lib/events/callanswerevent.h | 3 +-- lib/events/callhangupevent.h | 2 +- lib/events/callinviteevent.h | 2 +- lib/events/directchatevent.h | 2 +- lib/events/encryptedevent.h | 2 +- lib/events/encryptionevent.h | 5 ++--- lib/events/event.h | 23 +++++++++++++---------- lib/events/eventcontent.h | 19 ++++++++++--------- lib/events/reactionevent.h | 6 +++--- lib/events/receiptevent.h | 2 +- lib/events/roomavatarevent.h | 3 ++- lib/events/roomcreateevent.h | 2 +- lib/events/roomevent.h | 4 ++-- lib/events/roomkeyevent.h | 2 +- lib/events/roommemberevent.h | 5 ++--- lib/events/roommessageevent.h | 8 ++++---- lib/events/roompowerlevelsevent.h | 8 +++----- lib/events/roomtombstoneevent.h | 2 +- lib/events/simplestateevents.h | 3 ++- lib/events/stateevent.h | 2 +- lib/events/stickerevent.h | 2 +- lib/events/typingevent.h | 2 +- lib/eventstats.h | 4 ++-- lib/jobs/basejob.h | 4 ++-- lib/jobs/downloadfilejob.h | 2 +- lib/jobs/mediathumbnailjob.h | 2 +- lib/jobs/requestdata.h | 4 +++- lib/mxcreply.h | 4 +++- lib/networkaccessmanager.h | 4 +++- lib/networksettings.h | 2 +- lib/quotient_common.h | 17 ++++++++++++++++- lib/room.h | 8 ++++---- lib/settings.h | 8 +++++--- lib/ssosession.h | 4 +++- lib/syncdata.h | 2 +- lib/uri.h | 2 +- lib/uriresolver.h | 6 +++--- lib/user.h | 3 ++- lib/util.h | 22 ++++++++++++---------- 46 files changed, 138 insertions(+), 100 deletions(-) diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 5efda459..f7a864df 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -4,6 +4,8 @@ #pragma once +#include "quotient_export.h" + #include #include #include @@ -11,7 +13,7 @@ namespace Quotient { class Connection; -class AccountRegistry : public QAbstractListModel { +class QUOTIENT_API AccountRegistry : public QAbstractListModel { Q_OBJECT public: enum EventRoles { diff --git a/lib/avatar.h b/lib/avatar.h index d4634aea..93f43948 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -12,7 +14,7 @@ namespace Quotient { class Connection; -class Avatar { +class QUOTIENT_API Avatar { public: explicit Avatar(); explicit Avatar(QUrl url); diff --git a/lib/connection.h b/lib/connection.h index 0713af16..d669462e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -105,7 +105,7 @@ using DirectChatsMap = QMultiHash; using DirectChatUsersMap = QMultiHash; using IgnoredUsersList = IgnoredUsersEvent::content_type; -class Connection : public QObject { +class QUOTIENT_API Connection : public QObject { Q_OBJECT Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) diff --git a/lib/converters.h b/lib/converters.h index 9c3d5749..a6028f1b 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -183,7 +183,7 @@ struct JsonConverter { }; template <> -struct JsonConverter { +struct QUOTIENT_API JsonConverter { static QJsonValue dump(const QVariant& v); static QVariant load(const QJsonValue& jv); }; @@ -281,9 +281,9 @@ template struct JsonObjectConverter> : public HashMapFromJson> {}; -QJsonObject toJson(const QVariantHash& vh); +QJsonObject QUOTIENT_API toJson(const QVariantHash& vh); template <> -QVariantHash fromJson(const QJsonValue& jv); +QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 4f1595bc..a2d65d8d 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -25,3 +25,7 @@ void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) } setStatus(EventStatus::FileUploaded); } + +// Not exactly sure why but this helps with the linker not finding +// Quotient::EventStatus::staticMetaObject when building Quaternion +#include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index 0ab1a01d..2f1e72cc 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -4,6 +4,7 @@ #pragma once #include "events/stateevent.h" +#include "quotient_common.h" #include #include @@ -11,7 +12,7 @@ namespace Quotient { namespace EventStatus { - Q_NAMESPACE + QUO_NAMESPACE /** Special marks an event can assume * @@ -33,7 +34,7 @@ namespace EventStatus { Q_ENUM_NS(Code) } // namespace EventStatus -class EventItemBase { +class QUOTIENT_API EventItemBase { public: explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e)) { @@ -74,7 +75,7 @@ private: std::any data; }; -class TimelineItem : public EventItemBase { +class QUOTIENT_API TimelineItem : public EventItemBase { public: // For compatibility with Qt containers, even though we use // a std:: container now for the room timeline @@ -103,7 +104,7 @@ inline const CallEventBase* EventItemBase::viewAs() const return evt->isCallEvent() ? weakPtrCast(evt) : nullptr; } -class PendingEventItem : public EventItemBase { +class QUOTIENT_API PendingEventItem : public EventItemBase { public: using EventItemBase::EventItemBase; diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 9cf77be3..c0f2202d 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -50,7 +50,7 @@ struct JsonObjectConverter { using TagsMap = QHash; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public Event { \ + class QUOTIENT_API _Name : public Event { \ public: \ using content_type = _ContentType; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 4c01c941..8ffe60f2 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -7,7 +7,7 @@ #include "roomevent.h" namespace Quotient { -class CallAnswerEvent : public CallEventBase { +class QUOTIENT_API CallAnswerEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) @@ -26,6 +26,5 @@ public: return contentPart("answer"_ls).value("sdp"_ls).toString(); } }; - REGISTER_EVENT_TYPE(CallAnswerEvent) } // namespace Quotient diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index f3f82833..b0017c59 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -7,7 +7,7 @@ #include "roomevent.h" namespace Quotient { -class CallHangupEvent : public CallEventBase { +class QUOTIENT_API CallHangupEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 80b7d651..47362b5c 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -7,7 +7,7 @@ #include "roomevent.h" namespace Quotient { -class CallInviteEvent : public CallEventBase { +class QUOTIENT_API CallInviteEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index e2143779..2018d3d6 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -6,7 +6,7 @@ #include "event.h" namespace Quotient { -class DirectChatEvent : public Event { +class QUOTIENT_API DirectChatEvent : public Event { public: DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index de89a7c6..81343a29 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -25,7 +25,7 @@ namespace Quotient { * in general. It's possible, because RoomEvent interface is similar to Event's * one and doesn't add new restrictions, just provides additional features. */ -class EncryptedEvent : public RoomEvent { +class QUOTIENT_API EncryptedEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 14439fcc..dfb28b2f 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -8,7 +8,7 @@ #include "stateevent.h" namespace Quotient { -class EncryptionEventContent : public EventContent::Base { +class QUOTIENT_API EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; @@ -26,7 +26,7 @@ protected: using EncryptionType = EncryptionEventContent::EncryptionType; -class EncryptionEvent : public StateEvent { +class QUOTIENT_API EncryptionEvent : public StateEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) @@ -51,6 +51,5 @@ public: int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } }; - REGISTER_EVENT_TYPE(EncryptionEvent) } // namespace Quotient diff --git a/lib/events/event.h b/lib/events/event.h index 8f62872d..8a0076d0 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -60,14 +60,14 @@ inline QJsonObject basicEventJson(const QString& matrixType, using event_type_t = size_t; using event_mtype_t = const char*; -class EventTypeRegistry { +class QUOTIENT_API EventTypeRegistry { public: ~EventTypeRegistry() = default; static event_type_t initializeTypeId(event_mtype_t matrixTypeId); template - static inline event_type_t initializeTypeId() + static event_type_t initializeTypeId() { return initializeTypeId(EventT::matrixTypeId()); } @@ -190,7 +190,7 @@ namespace _impl { // === Event === -class Event { +class QUOTIENT_API Event { public: using Type = event_type_t; static inline _impl::EventFactory factory { "Event" }; @@ -243,7 +243,7 @@ public: return fromJson(unsignedJson()[std::forward(key)]); } - friend QDebug operator<<(QDebug dbg, const Event& e) + friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): "; @@ -272,17 +272,20 @@ using Events = EventsArray; // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static constexpr event_mtype_t matrixTypeId() { return _Id; } \ - static auto typeId() { return Quotient::typeId<_Type>(); } \ +#define DEFINE_EVENT_TYPEID(_Id, _Type) \ + static QUOTIENT_EXPORT constexpr event_mtype_t matrixTypeId() \ + { \ + return _Id; \ + } \ + static QUOTIENT_EXPORT auto typeId() { return Quotient::typeId<_Type>(); } \ // End of macro // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphic event arrays -#define REGISTER_EVENT_TYPE(_Type) \ - [[maybe_unused]] inline const auto _factoryAdded##_Type = \ - _Type::factory.addMethod<_Type>(); \ +#define REGISTER_EVENT_TYPE(_Type) \ + [[maybe_unused]] QUOTIENT_API inline const auto _factoryAdded##_Type = \ + _Type::factory.addMethod<_Type>(); \ // End of macro // === Event loading === diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index f609a603..bfa7d926 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -6,14 +6,15 @@ // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). +#include "encryptedfile.h" +#include "quotient_export.h" + #include #include #include #include #include -#include "encryptedfile.h" - class QFileInfo; namespace Quotient { @@ -28,7 +29,7 @@ namespace EventContent { * assumed but not required that a content object can also be created * from plain data. */ - class Base { + class QUOTIENT_API Base { public: explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} virtual ~Base() = default; @@ -76,7 +77,7 @@ namespace EventContent { * * This class is not polymorphic. */ - class FileInfo { + class QUOTIENT_API FileInfo { public: FileInfo() = default; explicit FileInfo(const QFileInfo& fi); @@ -121,7 +122,7 @@ namespace EventContent { /** * A content info class for image content types: image, thumbnail, video */ - class ImageInfo : public FileInfo { + class QUOTIENT_API ImageInfo : public FileInfo { public: ImageInfo() = default; explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); @@ -146,7 +147,7 @@ namespace EventContent { * the JSON representation of event content; namely, * "info/thumbnail_url" and "info/thumbnail_info" fields are used. */ - class Thumbnail : public ImageInfo { + class QUOTIENT_API Thumbnail : public ImageInfo { public: Thumbnail() = default; // Allow empty thumbnails Thumbnail(const QJsonObject& infoJson, const Omittable &file = none); @@ -160,7 +161,7 @@ namespace EventContent { void fillInfoJson(QJsonObject* infoJson) const; }; - class TypedBase : public Base { + class QUOTIENT_API TypedBase : public Base { public: virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } @@ -183,7 +184,7 @@ namespace EventContent { * \tparam InfoT base info class */ template - class UrlBasedContent : public TypedBase, public InfoT { + class QUOTIENT_API UrlBasedContent : public TypedBase, public InfoT { public: using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) @@ -215,7 +216,7 @@ namespace EventContent { }; template - class UrlWithThumbnailContent : public UrlBasedContent { + class QUOTIENT_API UrlWithThumbnailContent : public UrlBasedContent { public: // NB: when using inherited constructors, thumbnail has to be // initialised separately diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 5a2b98c4..ce11eaed 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -7,7 +7,7 @@ namespace Quotient { -struct EventRelation { +struct QUOTIENT_API EventRelation { using reltypeid_t = const char*; static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } static constexpr reltypeid_t Annotation() { return "m.annotation"; } @@ -31,12 +31,12 @@ struct EventRelation { } }; template <> -struct JsonObjectConverter { +struct QUOTIENT_API JsonObjectConverter { static void dumpTo(QJsonObject& jo, const EventRelation& pod); static void fillFrom(const QJsonObject& jo, EventRelation& pod); }; -class ReactionEvent : public RoomEvent { +class QUOTIENT_API ReactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index 9683deef..5e077e47 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -19,7 +19,7 @@ struct ReceiptsForEvent { }; using EventsWithReceipts = QVector; -class ReceiptEvent : public Event { +class QUOTIENT_API ReceiptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) explicit ReceiptEvent(const EventsWithReceipts& ewrs); diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 8618ba31..c54b5801 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -7,7 +7,8 @@ #include "stateevent.h" namespace Quotient { -class RoomAvatarEvent : public StateEvent { +class QUOTIENT_API RoomAvatarEvent + : public StateEvent { // It's a bit of an overkill to use a full-fledged ImageContent // because in reality m.room.avatar usually only has a single URL, // without a thumbnail. But The Spec says there be thumbnails, and diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index b3ad287c..016855b9 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -7,7 +7,7 @@ #include "quotient_common.h" namespace Quotient { -class RoomCreateEvent : public StateEventBase { +class QUOTIENT_API RoomCreateEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 8be58481..3fbb247e 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -11,7 +11,7 @@ namespace Quotient { class RedactionEvent; /** This class corresponds to m.room.* events */ -class RoomEvent : public Event { +class QUOTIENT_API RoomEvent : public Event { public: static inline _impl::EventFactory factory { "RoomEvent" }; @@ -70,7 +70,7 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = Range; -class CallEventBase : public RoomEvent { +class QUOTIENT_API CallEventBase : public RoomEvent { public: CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson = {}); diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index d021fbec..c4df7936 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -6,7 +6,7 @@ #include "event.h" namespace Quotient { -class RoomKeyEvent : public Event +class QUOTIENT_API RoomKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 0fb464d4..5e446dbe 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -10,7 +10,7 @@ #include "quotient_common.h" namespace Quotient { -class MemberEventContent : public EventContent::Base { +class QUOTIENT_API MemberEventContent : public EventContent::Base { public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; @@ -33,7 +33,7 @@ protected: using MembershipType [[deprecated("Use Membership instead")]] = Membership; -class RoomMemberEvent : public StateEvent { +class QUOTIENT_API RoomMemberEvent : public StateEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) @@ -95,6 +95,5 @@ doLoadEvent(const QJsonObject& json, const QString& matrixType) return makeEvent(json); return makeEvent(unknownEventTypeId(), json); } - REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 56597ddc..0c901b7a 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -16,7 +16,7 @@ namespace MessageEventContent = EventContent; // Back-compatibility /** * The event class corresponding to m.room.message events */ -class RoomMessageEvent : public RoomEvent { +class QUOTIENT_API RoomMessageEvent : public RoomEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) @@ -120,7 +120,7 @@ namespace EventContent { * Available fields: mimeType, body. The body can be either rich text * or plain text, depending on what mimeType specifies. */ - class TextContent : public TypedBase { + class QUOTIENT_API TextContent : public TypedBase { public: TextContent(QString text, const QString& contentType, Omittable relatesTo = none); @@ -149,7 +149,7 @@ namespace EventContent { * - thumbnail.mimeType * - thumbnail.imageSize */ - class LocationContent : public TypedBase { + class QUOTIENT_API LocationContent : public TypedBase { public: LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); @@ -168,7 +168,7 @@ namespace EventContent { * A base class for info types that include duration: audio and video */ template - class PlayableContent : public ContentT { + class QUOTIENT_API PlayableContent : public ContentT { public: using ContentT::ContentT; PlayableContent(const QJsonObject& json) diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index 0346fc0d..80e27048 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -7,7 +7,7 @@ #include "stateevent.h" namespace Quotient { -class PowerLevelsEventContent : public EventContent::Base { +class QUOTIENT_API PowerLevelsEventContent : public EventContent::Base { public: struct Notifications { int room; @@ -34,7 +34,8 @@ protected: void fillJson(QJsonObject* o) const override; }; -class RoomPowerLevelsEvent : public StateEvent { +class QUOTIENT_API RoomPowerLevelsEvent + : public StateEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent) @@ -61,9 +62,6 @@ public: int powerLevelForEvent(const QString& eventId) const; int powerLevelForState(const QString& eventId) const; int powerLevelForUser(const QString& userId) const; - -private: }; - REGISTER_EVENT_TYPE(RoomPowerLevelsEvent) } // namespace Quotient diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 30e53738..e336c448 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -6,7 +6,7 @@ #include "stateevent.h" namespace Quotient { -class RoomTombstoneEvent : public StateEventBase { +class QUOTIENT_API RoomTombstoneEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 9ce78609..d6557012 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -30,7 +30,8 @@ namespace EventContent { } // namespace EventContent #define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - class _Name : public StateEvent> { \ + class QUOTIENT_API _Name \ + : public StateEvent> { \ public: \ using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index c37965aa..6095d628 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -17,7 +17,7 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, { ContentKey, content } }; } -class StateEventBase : public RoomEvent { +class QUOTIENT_API StateEventBase : public RoomEvent { public: static inline _impl::EventFactory factory { "StateEvent" }; diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h index 93671086..0957dca3 100644 --- a/lib/events/stickerevent.h +++ b/lib/events/stickerevent.h @@ -11,7 +11,7 @@ namespace Quotient { /// Sticker messages are specialised image messages that are displayed without /// controls (e.g. no "download" link, or light-box view on click, as would be /// displayed for for m.image events). -class StickerEvent : public RoomEvent +class QUOTIENT_API StickerEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.sticker", StickerEvent) diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 7456100a..522f7e42 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -6,7 +6,7 @@ #include "event.h" namespace Quotient { -class TypingEvent : public Event { +class QUOTIENT_API TypingEvent : public Event { public: DEFINE_EVENT_TYPEID("m.typing", TypingEvent) diff --git a/lib/eventstats.h b/lib/eventstats.h index 77c661a7..a10c81fb 100644 --- a/lib/eventstats.h +++ b/lib/eventstats.h @@ -16,7 +16,7 @@ namespace Quotient { //! \note It's just a simple grouping of counters and is not automatically //! updated from the room as subsequent syncs arrive. //! \sa Room::unreadStats, Room::partiallyReadStats, Room::isEventNotable -struct EventStats { +struct QUOTIENT_API EventStats { Q_GADGET Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT) Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT) @@ -109,6 +109,6 @@ public: bool isValidFor(const Room* room, const marker_t& marker) const; }; -QDebug operator<<(QDebug dbg, const EventStats& es); +QUOTIENT_API QDebug operator<<(QDebug dbg, const EventStats& es); } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index ddf243ed..f41fc63c 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -20,7 +20,7 @@ class ConnectionData; enum class HttpVerb { Get, Put, Post, Delete }; -class BaseJob : public QObject { +class QUOTIENT_API BaseJob : public QObject { Q_OBJECT Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) @@ -470,7 +470,7 @@ private: QScopedPointer d; }; -inline bool isJobPending(BaseJob* job) +inline bool QUOTIENT_API isJobPending(BaseJob* job) { return job && job->error() == BaseJob::Pending; } diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index 0752af89..d9f3b686 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -6,7 +6,7 @@ #include "csapi/content-repo.h" namespace Quotient { -class DownloadFileJob : public GetContentJob { +class QUOTIENT_API DownloadFileJob : public GetContentJob { public: using GetContentJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index 3183feb1..c9f6da35 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -8,7 +8,7 @@ #include namespace Quotient { -class MediaThumbnailJob : public GetContentThumbnailJob { +class QUOTIENT_API MediaThumbnailJob : public GetContentThumbnailJob { public: using GetContentThumbnailJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 4f05e5ff..41ad833a 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -19,7 +21,7 @@ namespace Quotient { * as well as JSON (and possibly other structures in the future) to * a QByteArray consumed by QNetworkAccessManager request methods. */ -class RequestData { +class QUOTIENT_API RequestData { public: RequestData(const QByteArray& a = {}); RequestData(const QJsonObject& jo); diff --git a/lib/mxcreply.h b/lib/mxcreply.h index efaf01c6..23049b7d 100644 --- a/lib/mxcreply.h +++ b/lib/mxcreply.h @@ -3,13 +3,15 @@ #pragma once +#include "quotient_export.h" + #include #include namespace Quotient { class Room; -class MxcReply : public QNetworkReply +class QUOTIENT_API MxcReply : public QNetworkReply { public: explicit MxcReply(); diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 87bc12a1..d06f9736 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -10,7 +12,7 @@ namespace Quotient { class Room; class Connection; -class NetworkAccessManager : public QNetworkAccessManager { +class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: NetworkAccessManager(QObject* parent = nullptr); diff --git a/lib/networksettings.h b/lib/networksettings.h index df11a9c8..c1446355 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -10,7 +10,7 @@ Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) namespace Quotient { -class NetworkSettings : public SettingsGroup { +class QUOTIENT_API NetworkSettings : public SettingsGroup { Q_OBJECT QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 0e3e2a40..a5926e8c 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -26,8 +28,21 @@ #define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) +// The first line is a usual way to indicate a namespace to moc; +// the second line redeclares the namespace static metaobject with +// QUOTIENT_API so that dynamically linked clients could serialise +// flag/enum values from the namespace. +#define QUO_NAMESPACE \ +Q_NAMESPACE \ +extern QUOTIENT_API const QMetaObject staticMetaObject; +#else +// Since Qt 5.14.0, it's all packed in a single macro +#define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API) +#endif + namespace Quotient { -Q_NAMESPACE +QUO_NAMESPACE // std::array {} needs explicit template parameters on macOS because // Apple stdlib doesn't have deduction guides for std::array. C++20 has diff --git a/lib/room.h b/lib/room.h index 85c51a87..63a4aaea 100644 --- a/lib/room.h +++ b/lib/room.h @@ -45,7 +45,7 @@ class RedactEventJob; * This is specifically tuned to work with QML exposing all traits as * Q_PROPERTY values. */ -class FileTransferInfo { +class QUOTIENT_API FileTransferInfo { Q_GADGET Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) @@ -73,7 +73,7 @@ public: //! \brief Data structure for a room member's read receipt //! \sa Room::lastReadReceipt -class ReadReceipt { +class QUOTIENT_API ReadReceipt { Q_GADGET Q_PROPERTY(QString eventId MEMBER eventId CONSTANT) Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT) @@ -110,7 +110,7 @@ private: Q_PROPERTY(Type type MEMBER type CONSTANT) }; -class Room : public QObject { +class QUOTIENT_API Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) @@ -1037,7 +1037,7 @@ private: void setJoinState(JoinState state); }; -class MemberSorter { +class QUOTIENT_API MemberSorter { public: explicit MemberSorter(const Room* r) : room(r) {} diff --git a/lib/settings.h b/lib/settings.h index efd0d714..b66879c5 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include #include #include @@ -11,7 +13,7 @@ class QVariant; namespace Quotient { -class Settings : public QSettings { +class QUOTIENT_API Settings : public QSettings { Q_OBJECT public: /// Add a legacy organisation/application name to migrate settings from @@ -76,7 +78,7 @@ protected: QSettings legacySettings { legacyOrganizationName, legacyApplicationName }; }; -class SettingsGroup : public Settings { +class QUOTIENT_API SettingsGroup : public Settings { public: explicit SettingsGroup(QString path, QObject* parent = nullptr) : Settings(parent) @@ -124,7 +126,7 @@ private: setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } -class AccountSettings : public SettingsGroup { +class QUOTIENT_API AccountSettings : public SettingsGroup { Q_OBJECT Q_PROPERTY(QString userId READ userId CONSTANT) QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) diff --git a/lib/ssosession.h b/lib/ssosession.h index 72dd60c4..a658c043 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -29,7 +31,7 @@ class Connection; * connection->prepareForSso(initialDeviceName)->ssoUrl()); * \endcode */ -class SsoSession : public QObject { +class QUOTIENT_API SsoSession : public QObject { Q_OBJECT Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT) Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT) diff --git a/lib/syncdata.h b/lib/syncdata.h index 36d2e0bf..e29540c2 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -22,7 +22,7 @@ constexpr auto HighlightCountKey = "highlight_count"_ls; * means that nothing has come from the server; heroes.value().isEmpty() * means a peculiar case of a room with the only member - the current user. */ -struct RoomSummary { +struct QUOTIENT_API RoomSummary { Omittable joinedMemberCount; Omittable invitedMemberCount; Omittable heroes; //< mxids of users to take part in the room diff --git a/lib/uri.h b/lib/uri.h index d8b892b6..78cd27c8 100644 --- a/lib/uri.h +++ b/lib/uri.h @@ -23,7 +23,7 @@ namespace Quotient { * its type, and obtain components, also in either unencoded (for displaying) * or encoded (for APIs) form. */ -class Uri : private QUrl { +class QUOTIENT_API Uri : private QUrl { Q_GADGET public: enum Type : char { diff --git a/lib/uriresolver.h b/lib/uriresolver.h index ff97324a..9140046c 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -25,7 +25,7 @@ class User; * gradual implementation. Derived classes are encouraged to override as many * of them as possible. */ -class UriResolverBase { +class QUOTIENT_API UriResolverBase { public: /*! \brief Resolve the resource and dispatch an action depending on its type * @@ -105,7 +105,7 @@ protected: * * \sa UriResolverBase, UriDispatcher */ -UriResolveResult +QUOTIENT_API UriResolveResult visitResource(Connection* account, const Uri& uri, std::function userHandler, std::function roomEventHandler, @@ -141,7 +141,7 @@ inline UriResolveResult checkResource(Connection* account, const Uri& uri) * synchronously - the returned value is the result of resolving the URI, * not acting on it. */ -class UriDispatcher : public QObject, public UriResolverBase { +class QUOTIENT_API UriDispatcher : public QObject, public UriResolverBase { Q_OBJECT public: explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} diff --git a/lib/user.h b/lib/user.h index 78b72bf2..435304ce 100644 --- a/lib/user.h +++ b/lib/user.h @@ -5,6 +5,7 @@ #pragma once #include "avatar.h" +#include "quotient_export.h" #include @@ -13,7 +14,7 @@ class Connection; class Room; class RoomMemberEvent; -class User : public QObject { +class QUOTIENT_API User : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(bool isGuest READ isGuest CONSTANT) diff --git a/lib/util.h b/lib/util.h index 97f0ecbc..399f93c2 100644 --- a/lib/util.h +++ b/lib/util.h @@ -4,6 +4,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -244,26 +246,26 @@ inline std::pair findFirstOf(InputIt first, InputIt last, } /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ -void linkifyUrls(QString& htmlEscapedText); +QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); /** Sanitize the text before showing in HTML * * This does toHtmlEscaped() and removes Unicode BiDi marks. */ -QString sanitized(const QString& plainText); +QUOTIENT_API QString sanitized(const QString& plainText); /** Pretty-print plain text into HTML * * This includes HTML escaping of <,>,",& and calling linkifyUrls() */ -QString prettyPrint(const QString& plainText); +QUOTIENT_API QString prettyPrint(const QString& plainText); /** Return a path to cache directory after making sure that it exists * * The returned path has a trailing slash, clients don't need to append it. * \param dir path to cache directory relative to the standard cache path */ -QString cacheLocation(const QString& dirName); +QUOTIENT_API QString cacheLocation(const QString& dirName); /** Hue color component of based of the hash of the string. * @@ -272,13 +274,13 @@ QString cacheLocation(const QString& dirName); * Naming and range are the same as QColor's hueF method: * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision */ -qreal stringToHueF(const QString& s); +QUOTIENT_API qreal stringToHueF(const QString& s); /** Extract the serverpart from MXID */ -QString serverPart(const QString& mxId); +QUOTIENT_API QString serverPart(const QString& mxId); -QString versionString(); -int majorVersion(); -int minorVersion(); -int patchVersion(); +QUOTIENT_API QString versionString(); +QUOTIENT_API int majorVersion(); +QUOTIENT_API int minorVersion(); +QUOTIENT_API int patchVersion(); } // namespace Quotient -- cgit v1.2.3 From f69e1c52246b986fb91f595ee0ee363e5343dbbb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 29 Dec 2021 15:57:14 +0100 Subject: operation.h.mustache: Add QUOTIENT_API --- gtad/operation.h.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/operation.h.mustache b/gtad/operation.h.mustache index f91dc66c..063f0bbd 100644 --- a/gtad/operation.h.mustache +++ b/gtad/operation.h.mustache @@ -16,7 +16,7 @@ namespace Quotient { /*!{{>docCommentSummary}}{{#description}} * {{_}}{{/description}} */ -class {{camelCaseOperationId}}Job : public BaseJob { +class QUOTIENT_API {{camelCaseOperationId}}Job : public BaseJob { public: {{#models}} // Inner data structures -- cgit v1.2.3 From 952f8aa8ad19348b50a3b3545d98f7889bfdae76 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 29 Dec 2021 15:57:33 +0100 Subject: Regenerate CS API files --- lib/csapi/account-data.h | 8 ++++---- lib/csapi/admin.h | 2 +- lib/csapi/administrative_contact.h | 16 ++++++++-------- lib/csapi/appservice_room_directory.h | 3 ++- lib/csapi/banning.h | 4 ++-- lib/csapi/capabilities.h | 2 +- lib/csapi/content-repo.h | 12 ++++++------ lib/csapi/create_room.h | 2 +- lib/csapi/cross_signing.h | 4 ++-- lib/csapi/device_management.h | 10 +++++----- lib/csapi/directory.h | 8 ++++---- lib/csapi/event_context.h | 2 +- lib/csapi/filter.h | 4 ++-- lib/csapi/inviting.h | 2 +- lib/csapi/joining.h | 4 ++-- lib/csapi/keys.h | 8 ++++---- lib/csapi/kicking.h | 2 +- lib/csapi/knocking.h | 2 +- lib/csapi/leaving.h | 4 ++-- lib/csapi/list_joined_rooms.h | 2 +- lib/csapi/list_public_rooms.h | 8 ++++---- lib/csapi/login.h | 4 ++-- lib/csapi/logout.h | 4 ++-- lib/csapi/message_pagination.h | 2 +- lib/csapi/notifications.h | 2 +- lib/csapi/openid.h | 2 +- lib/csapi/peeking_events.h | 2 +- lib/csapi/presence.h | 4 ++-- lib/csapi/profile.h | 10 +++++----- lib/csapi/pusher.h | 4 ++-- lib/csapi/pushrules.h | 16 ++++++++-------- lib/csapi/read_markers.h | 2 +- lib/csapi/receipts.h | 2 +- lib/csapi/redaction.h | 2 +- lib/csapi/registration.h | 16 ++++++++-------- lib/csapi/report_content.h | 2 +- lib/csapi/room_send.h | 2 +- lib/csapi/room_state.h | 2 +- lib/csapi/room_upgrades.h | 2 +- lib/csapi/rooms.h | 10 +++++----- lib/csapi/search.h | 2 +- lib/csapi/sso_login_redirect.h | 4 ++-- lib/csapi/tags.h | 6 +++--- lib/csapi/third_party_lookup.h | 12 ++++++------ lib/csapi/third_party_membership.h | 2 +- lib/csapi/to_device.h | 2 +- lib/csapi/typing.h | 2 +- lib/csapi/users.h | 2 +- lib/csapi/versions.h | 2 +- lib/csapi/voip.h | 2 +- lib/csapi/wellknown.h | 2 +- lib/csapi/whoami.h | 2 +- 52 files changed, 120 insertions(+), 119 deletions(-) diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index 0c760e80..5140d340 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -14,7 +14,7 @@ namespace Quotient { * that set the account_data. The config will be synced to clients in the * top-level `account_data`. */ -class SetAccountDataJob : public BaseJob { +class QUOTIENT_API SetAccountDataJob : public BaseJob { public: /*! \brief Set some account_data for the user. * @@ -38,7 +38,7 @@ public: * Get some account_data for the client. This config is only visible to the user * that set the account_data. */ -class GetAccountDataJob : public BaseJob { +class QUOTIENT_API GetAccountDataJob : public BaseJob { public: /*! \brief Get some account_data for the user. * @@ -67,7 +67,7 @@ public: * visible to the user that set the account_data. The config will be synced to * clients in the per-room `account_data`. */ -class SetAccountDataPerRoomJob : public BaseJob { +class QUOTIENT_API SetAccountDataPerRoomJob : public BaseJob { public: /*! \brief Set some account_data for the user. * @@ -95,7 +95,7 @@ public: * Get some account_data for the client on a given room. This config is only * visible to the user that set the account_data. */ -class GetAccountDataPerRoomJob : public BaseJob { +class QUOTIENT_API GetAccountDataPerRoomJob : public BaseJob { public: /*! \brief Get some account_data for the user. * diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index 570bf24a..c53ddd7e 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -16,7 +16,7 @@ namespace Quotient { * up, or by a server admin. Server-local administrator privileges are not * specified in this document. */ -class GetWhoIsJob : public BaseJob { +class QUOTIENT_API GetWhoIsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index e436971d..e636b12a 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -24,7 +24,7 @@ namespace Quotient { * Identifiers in this list may be used by the homeserver as, for example, * identifiers that it will accept to reset the user's account password. */ -class GetAccount3PIDsJob : public BaseJob { +class QUOTIENT_API GetAccount3PIDsJob : public BaseJob { public: // Inner data structures @@ -102,7 +102,7 @@ struct JsonObjectConverter { * This results in this endpoint being an equivalent to `/3pid/bind` rather * than dual-purpose. */ -class Post3PIDsJob : public BaseJob { +class QUOTIENT_API Post3PIDsJob : public BaseJob { public: // Inner data structures @@ -154,7 +154,7 @@ struct JsonObjectConverter { * Homeservers should prevent the caller from adding a 3PID to their account if * it has already been added to another user's account on the homeserver. */ -class Add3PIDJob : public BaseJob { +class QUOTIENT_API Add3PIDJob : public BaseJob { public: /*! \brief Adds contact information to the user's account. * @@ -182,7 +182,7 @@ public: * * Homeservers should track successful binds so they can be unbound later. */ -class Bind3PIDJob : public BaseJob { +class QUOTIENT_API Bind3PIDJob : public BaseJob { public: /*! \brief Binds a 3PID to the user's account through an Identity Service. * @@ -211,7 +211,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class Delete3pidFromAccountJob : public BaseJob { +class QUOTIENT_API Delete3pidFromAccountJob : public BaseJob { public: /*! \brief Deletes a third party identifier from the user's account * @@ -254,7 +254,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class Unbind3pidFromAccountJob : public BaseJob { +class QUOTIENT_API Unbind3pidFromAccountJob : public BaseJob { public: /*! \brief Removes a user's third party identifier from an identity server. * @@ -300,7 +300,7 @@ public: * the email itself, either by sending a validation email itself or by using * a service it has control over. */ -class RequestTokenTo3PIDEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenTo3PIDEmailJob : public BaseJob { public: /*! \brief Begins the validation process for an email address for * association with the user's account. @@ -342,7 +342,7 @@ public: * the phone number itself, either by sending a validation message itself or by * using a service it has control over. */ -class RequestTokenTo3PIDMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenTo3PIDMSISDNJob : public BaseJob { public: /*! \brief Begins the validation process for a phone number for association * with the user's account. diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index 56a69592..6b2801ca 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -21,7 +21,8 @@ namespace Quotient { * instead of a typical client's access_token. This API cannot be invoked by * users who are not identified as application services. */ -class UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { +class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob + : public BaseJob { public: /*! \brief Updates a room's visibility in the application service's room * directory. diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 7a9697d3..e4c60ce3 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -18,7 +18,7 @@ namespace Quotient { * The caller must have the required power level in order to perform this * operation. */ -class BanJob : public BaseJob { +class QUOTIENT_API BanJob : public BaseJob { public: /*! \brief Ban a user in the room. * @@ -46,7 +46,7 @@ public: * The caller must have the required power level in order to perform this * operation. */ -class UnbanJob : public BaseJob { +class QUOTIENT_API UnbanJob : public BaseJob { public: /*! \brief Unban a user from the room. * diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index da50c8c1..81b47cd4 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -13,7 +13,7 @@ namespace Quotient { * Gets information about the server's supported feature set * and other relevant capabilities. */ -class GetCapabilitiesJob : public BaseJob { +class QUOTIENT_API GetCapabilitiesJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index 28409f5c..511db985 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -14,7 +14,7 @@ namespace Quotient { /*! \brief Upload some content to the content repository. * */ -class UploadContentJob : public BaseJob { +class QUOTIENT_API UploadContentJob : public BaseJob { public: /*! \brief Upload some content to the content repository. * @@ -40,7 +40,7 @@ public: /*! \brief Download content from the content repository. * */ -class GetContentJob : public BaseJob { +class QUOTIENT_API GetContentJob : public BaseJob { public: /*! \brief Download content from the content repository. * @@ -87,7 +87,7 @@ public: * the previous endpoint) but replace the target file name with the one * provided by the caller. */ -class GetContentOverrideNameJob : public BaseJob { +class QUOTIENT_API GetContentOverrideNameJob : public BaseJob { public: /*! \brief Download content from the content repository overriding the file * name @@ -142,7 +142,7 @@ public: * See the [Thumbnails](/client-server-api/#thumbnails) section for more * information. */ -class GetContentThumbnailJob : public BaseJob { +class QUOTIENT_API GetContentThumbnailJob : public BaseJob { public: /*! \brief Download a thumbnail of content from the content repository * @@ -204,7 +204,7 @@ public: * do not want to share with the homeserver, and this can mean that the URLs * being shared should also not be shared with the homeserver. */ -class GetUrlPreviewJob : public BaseJob { +class QUOTIENT_API GetUrlPreviewJob : public BaseJob { public: /*! \brief Get information about a URL for a client * @@ -252,7 +252,7 @@ public: * content repository APIs, for example, proxies may enforce a lower upload size * limit than is advertised by the server on this endpoint. */ -class GetConfigJob : public BaseJob { +class QUOTIENT_API GetConfigJob : public BaseJob { public: /// Get the configuration for the content repository. explicit GetConfigJob(); diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 81dfbffc..7d566057 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -53,7 +53,7 @@ namespace Quotient { * requesting user as the creator, alongside other keys provided in the * `creation_content`. */ -class CreateRoomJob : public BaseJob { +class QUOTIENT_API CreateRoomJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h index 2ab65e06..617b61d1 100644 --- a/lib/csapi/cross_signing.h +++ b/lib/csapi/cross_signing.h @@ -17,7 +17,7 @@ namespace Quotient { * This API endpoint uses the [User-Interactive Authentication * API](/client-server-api/#user-interactive-authentication-api). */ -class UploadCrossSigningKeysJob : public BaseJob { +class QUOTIENT_API UploadCrossSigningKeysJob : public BaseJob { public: /*! \brief Upload cross-signing keys. * @@ -47,7 +47,7 @@ public: * Publishes cross-signing signatures for the user. The request body is a * map from user ID to key ID to signed JSON object. */ -class UploadCrossSigningSignaturesJob : public BaseJob { +class QUOTIENT_API UploadCrossSigningSignaturesJob : public BaseJob { public: /*! \brief Upload cross-signing signatures. * diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index 7fb69873..430d2132 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -15,7 +15,7 @@ namespace Quotient { * * Gets information about all devices for the current user. */ -class GetDevicesJob : public BaseJob { +class QUOTIENT_API GetDevicesJob : public BaseJob { public: /// List registered devices for the current user explicit GetDevicesJob(); @@ -40,7 +40,7 @@ public: * * Gets information on a single device, by device id. */ -class GetDeviceJob : public BaseJob { +class QUOTIENT_API GetDeviceJob : public BaseJob { public: /*! \brief Get a single device * @@ -66,7 +66,7 @@ public: * * Updates the metadata on the given device. */ -class UpdateDeviceJob : public BaseJob { +class QUOTIENT_API UpdateDeviceJob : public BaseJob { public: /*! \brief Update a device * @@ -88,7 +88,7 @@ public: * * Deletes the given device, and invalidates any access token associated with it. */ -class DeleteDeviceJob : public BaseJob { +class QUOTIENT_API DeleteDeviceJob : public BaseJob { public: /*! \brief Delete a device * @@ -111,7 +111,7 @@ public: * Deletes the given devices, and invalidates any access token associated with * them. */ -class DeleteDevicesJob : public BaseJob { +class QUOTIENT_API DeleteDevicesJob : public BaseJob { public: /*! \brief Bulk deletion of devices * diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 93a31595..0bd13a76 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -11,7 +11,7 @@ namespace Quotient { /*! \brief Create a new mapping from room alias to room ID. * */ -class SetRoomAliasJob : public BaseJob { +class QUOTIENT_API SetRoomAliasJob : public BaseJob { public: /*! \brief Create a new mapping from room alias to room ID. * @@ -32,7 +32,7 @@ public: * domain part of the alias does not correspond to the server's own * domain. */ -class GetRoomIdByAliasJob : public BaseJob { +class QUOTIENT_API GetRoomIdByAliasJob : public BaseJob { public: /*! \brief Get the room ID corresponding to this room alias. * @@ -76,7 +76,7 @@ public: * return a successful response even if the user does not have permission to * update the `m.room.canonical_alias` event. */ -class DeleteRoomAliasJob : public BaseJob { +class QUOTIENT_API DeleteRoomAliasJob : public BaseJob { public: /*! \brief Remove a mapping of room alias to room ID. * @@ -112,7 +112,7 @@ public: * as they are not curated, unlike those listed in the `m.room.canonical_alias` * state event. */ -class GetLocalAliasesJob : public BaseJob { +class QUOTIENT_API GetLocalAliasesJob : public BaseJob { public: /*! \brief Get a list of local aliases on a given room. * diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index 4e50edf3..662b976b 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -19,7 +19,7 @@ namespace Quotient { * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) * for more information. */ -class GetEventContextJob : public BaseJob { +class QUOTIENT_API GetEventContextJob : public BaseJob { public: /*! \brief Get events and state around the specified event. * diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index 01bec36b..9518a461 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -16,7 +16,7 @@ namespace Quotient { * Returns a filter ID that may be used in future requests to * restrict which events are returned to the client. */ -class DefineFilterJob : public BaseJob { +class QUOTIENT_API DefineFilterJob : public BaseJob { public: /*! \brief Upload a new filter. * @@ -41,7 +41,7 @@ public: /*! \brief Download a filter * */ -class GetFilterJob : public BaseJob { +class QUOTIENT_API GetFilterJob : public BaseJob { public: /*! \brief Download a filter * diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index eb13cc95..21e6cb74 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -26,7 +26,7 @@ namespace Quotient { * If the user was invited to the room, the homeserver will append a * `m.room.member` event to the room. */ -class InviteUserJob : public BaseJob { +class QUOTIENT_API InviteUserJob : public BaseJob { public: /*! \brief Invite a user to participate in a particular room. * diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index d0199b11..f64152f7 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -25,7 +25,7 @@ namespace Quotient { * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ -class JoinRoomByIdJob : public BaseJob { +class QUOTIENT_API JoinRoomByIdJob : public BaseJob { public: /*! \brief Start the requesting user participating in a particular room. * @@ -67,7 +67,7 @@ public: * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ -class JoinRoomJob : public BaseJob { +class QUOTIENT_API JoinRoomJob : public BaseJob { public: /*! \brief Start the requesting user participating in a particular room. * diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 7db09e8d..ce1ca9ed 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -15,7 +15,7 @@ namespace Quotient { * * Publishes end-to-end encryption keys for the device. */ -class UploadKeysJob : public BaseJob { +class QUOTIENT_API UploadKeysJob : public BaseJob { public: /*! \brief Upload end-to-end encryption keys. * @@ -48,7 +48,7 @@ public: * * Returns the current devices and identity keys for the given users. */ -class QueryKeysJob : public BaseJob { +class QUOTIENT_API QueryKeysJob : public BaseJob { public: // Inner data structures @@ -172,7 +172,7 @@ struct JsonObjectConverter { * * Claims one-time keys for use in pre-key messages. */ -class ClaimKeysJob : public BaseJob { +class QUOTIENT_API ClaimKeysJob : public BaseJob { public: /*! \brief Claim one-time encryption keys. * @@ -226,7 +226,7 @@ public: * * added new device identity keys or removed an existing device with * identity keys, between `from` and `to`. */ -class GetKeysChangesJob : public BaseJob { +class QUOTIENT_API GetKeysChangesJob : public BaseJob { public: /*! \brief Query users with recent device key updates. * diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 11018368..6ac106e2 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -20,7 +20,7 @@ namespace Quotient { * directly adjust the target member's state by making a request to * `/rooms//state/m.room.member/`. */ -class KickJob : public BaseJob { +class QUOTIENT_API KickJob : public BaseJob { public: /*! \brief Kick a user from the room. * diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h index 1108cb64..e3645b59 100644 --- a/lib/csapi/knocking.h +++ b/lib/csapi/knocking.h @@ -27,7 +27,7 @@ namespace Quotient { * The knock will appear as an entry in the response of the * [`/sync`](/client-server-api/#get_matrixclientr0sync) API. */ -class KnockRoomJob : public BaseJob { +class QUOTIENT_API KnockRoomJob : public BaseJob { public: /*! \brief Knock on a room, requesting permission to join. * diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 2e402d16..19cac3f0 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -22,7 +22,7 @@ namespace Quotient { * The user will still be allowed to retrieve history from the room which * they were previously allowed to see. */ -class LeaveRoomJob : public BaseJob { +class QUOTIENT_API LeaveRoomJob : public BaseJob { public: /*! \brief Stop the requesting user participating in a particular room. * @@ -48,7 +48,7 @@ public: * If the user is currently joined to the room, they must leave the room * before calling this API. */ -class ForgetRoomJob : public BaseJob { +class QUOTIENT_API ForgetRoomJob : public BaseJob { public: /*! \brief Stop the requesting user remembering about a particular room. * diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 59a24a49..aea68afd 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -12,7 +12,7 @@ namespace Quotient { * * This API returns a list of the user's current rooms. */ -class GetJoinedRoomsJob : public BaseJob { +class QUOTIENT_API GetJoinedRoomsJob : public BaseJob { public: /// Lists the user's current rooms. explicit GetJoinedRoomsJob(); diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 963c8b56..e1f03db7 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -14,7 +14,7 @@ namespace Quotient { * * Gets the visibility of a given room on the server's public room directory. */ -class GetRoomVisibilityOnDirectoryJob : public BaseJob { +class QUOTIENT_API GetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! \brief Gets the visibility of a room in the directory * @@ -48,7 +48,7 @@ public: * here, for instance that room visibility can only be changed by * the room creator or a server administrator. */ -class SetRoomVisibilityOnDirectoryJob : public BaseJob { +class QUOTIENT_API SetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! \brief Sets the visibility of a room in the room directory * @@ -70,7 +70,7 @@ public: * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ -class GetPublicRoomsJob : public BaseJob { +class QUOTIENT_API GetPublicRoomsJob : public BaseJob { public: /*! \brief Lists the public rooms on the server. * @@ -133,7 +133,7 @@ public: * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ -class QueryPublicRoomsJob : public BaseJob { +class QUOTIENT_API QueryPublicRoomsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/login.h b/lib/csapi/login.h index b35db1eb..ce6951eb 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -16,7 +16,7 @@ namespace Quotient { * Gets the homeserver's supported login types to authenticate users. Clients * should pick one of these and supply it as the `type` when logging in. */ -class GetLoginFlowsJob : public BaseJob { +class QUOTIENT_API GetLoginFlowsJob : public BaseJob { public: // Inner data structures @@ -73,7 +73,7 @@ struct JsonObjectConverter { * [Relationship between access tokens and * devices](/client-server-api/#relationship-between-access-tokens-and-devices). */ -class LoginJob : public BaseJob { +class QUOTIENT_API LoginJob : public BaseJob { public: /*! \brief Authenticates the user. * diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 2e4c2692..3f1ac7fa 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -15,7 +15,7 @@ namespace Quotient { * [Device keys](/client-server-api/#device-keys) for the device are deleted * alongside the device. */ -class LogoutJob : public BaseJob { +class QUOTIENT_API LogoutJob : public BaseJob { public: /// Invalidates a user access token explicit LogoutJob(); @@ -44,7 +44,7 @@ public: * used in the request, and therefore the attacker is unable to take over the * account in this way. */ -class LogoutAllJob : public BaseJob { +class QUOTIENT_API LogoutAllJob : public BaseJob { public: /// Invalidates all access tokens for a user explicit LogoutAllJob(); diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 363e4d99..8c18f104 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -18,7 +18,7 @@ namespace Quotient { * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) * for more information. */ -class GetRoomEventsJob : public BaseJob { +class QUOTIENT_API GetRoomEventsJob : public BaseJob { public: /*! \brief Get a list of events for this room * diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 0c38fe6b..23211758 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -14,7 +14,7 @@ namespace Quotient { * This API is used to paginate through the list of events that the * user has been, or would have been notified about. */ -class GetNotificationsJob : public BaseJob { +class QUOTIENT_API GetNotificationsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 0be39c8c..773b6011 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -21,7 +21,7 @@ namespace Quotient { * be used to request another OpenID access token or call `/sync`, for * example. */ -class RequestOpenIdTokenJob : public BaseJob { +class QUOTIENT_API RequestOpenIdTokenJob : public BaseJob { public: /*! \brief Get an OpenID token object to verify the requester's identity. * diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 885ff340..14cb6f0b 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -22,7 +22,7 @@ namespace Quotient { * API will also be deprecated at some point, but its replacement is not * yet known. */ -class PeekEventsJob : public BaseJob { +class QUOTIENT_API PeekEventsJob : public BaseJob { public: /*! \brief Listen on the event stream. * diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 4ab50e25..52445205 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -15,7 +15,7 @@ namespace Quotient { * not need to specify the `last_active_ago` field. You cannot set the * presence state of another user. */ -class SetPresenceJob : public BaseJob { +class QUOTIENT_API SetPresenceJob : public BaseJob { public: /*! \brief Update this user's presence state. * @@ -36,7 +36,7 @@ public: * * Get the given user's presence state. */ -class GetPresenceJob : public BaseJob { +class QUOTIENT_API GetPresenceJob : public BaseJob { public: /*! \brief Get this user's presence state. * diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 7f9c9e95..b00c944b 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -13,7 +13,7 @@ namespace Quotient { * This API sets the given user's display name. You must have permission to * set this user's display name, e.g. you need to have their `access_token`. */ -class SetDisplayNameJob : public BaseJob { +class QUOTIENT_API SetDisplayNameJob : public BaseJob { public: /*! \brief Set the user's display name. * @@ -33,7 +33,7 @@ public: * own displayname or to query the name of other users; either locally or * on remote homeservers. */ -class GetDisplayNameJob : public BaseJob { +class QUOTIENT_API GetDisplayNameJob : public BaseJob { public: /*! \brief Get the user's display name. * @@ -63,7 +63,7 @@ public: * This API sets the given user's avatar URL. You must have permission to * set this user's avatar URL, e.g. you need to have their `access_token`. */ -class SetAvatarUrlJob : public BaseJob { +class QUOTIENT_API SetAvatarUrlJob : public BaseJob { public: /*! \brief Set the user's avatar URL. * @@ -82,7 +82,7 @@ public: * own avatar URL or to query the URL of other users; either locally or * on remote homeservers. */ -class GetAvatarUrlJob : public BaseJob { +class QUOTIENT_API GetAvatarUrlJob : public BaseJob { public: /*! \brief Get the user's avatar URL. * @@ -111,7 +111,7 @@ public: * locally or on remote homeservers. This API may return keys which are not * limited to `displayname` or `avatar_url`. */ -class GetUserProfileJob : public BaseJob { +class QUOTIENT_API GetUserProfileJob : public BaseJob { public: /*! \brief Get this user's profile information. * diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 622b0df6..d859ffc4 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -12,7 +12,7 @@ namespace Quotient { * * Gets all currently active pushers for the authenticated user. */ -class GetPushersJob : public BaseJob { +class QUOTIENT_API GetPushersJob : public BaseJob { public: // Inner data structures @@ -108,7 +108,7 @@ struct JsonObjectConverter { * [pushers](/client-server-api/#push-notifications) for this user ID. The * behaviour of this endpoint varies depending on the values in the JSON body. */ -class PostPusherJob : public BaseJob { +class QUOTIENT_API PostPusherJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index a5eb48f0..d6c57efd 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -19,7 +19,7 @@ namespace Quotient { * `/pushrules/global/`. This will return a subset of this data under the * specified key e.g. the `global` key. */ -class GetPushRulesJob : public BaseJob { +class QUOTIENT_API GetPushRulesJob : public BaseJob { public: /// Retrieve all push rulesets. explicit GetPushRulesJob(); @@ -44,7 +44,7 @@ public: * * Retrieve a single specified push rule. */ -class GetPushRuleJob : public BaseJob { +class QUOTIENT_API GetPushRuleJob : public BaseJob { public: /*! \brief Retrieve a push rule. * @@ -79,7 +79,7 @@ public: * * This endpoint removes the push rule defined in the path. */ -class DeletePushRuleJob : public BaseJob { +class QUOTIENT_API DeletePushRuleJob : public BaseJob { public: /*! \brief Delete a push rule. * @@ -112,7 +112,7 @@ public: * * When creating push rules, they MUST be enabled by default. */ -class SetPushRuleJob : public BaseJob { +class QUOTIENT_API SetPushRuleJob : public BaseJob { public: /*! \brief Add or change a push rule. * @@ -160,7 +160,7 @@ public: * * This endpoint gets whether the specified push rule is enabled. */ -class IsPushRuleEnabledJob : public BaseJob { +class QUOTIENT_API IsPushRuleEnabledJob : public BaseJob { public: /*! \brief Get whether a push rule is enabled * @@ -195,7 +195,7 @@ public: * * This endpoint allows clients to enable or disable the specified push rule. */ -class SetPushRuleEnabledJob : public BaseJob { +class QUOTIENT_API SetPushRuleEnabledJob : public BaseJob { public: /*! \brief Enable or disable a push rule. * @@ -219,7 +219,7 @@ public: * * This endpoint get the actions for the specified push rule. */ -class GetPushRuleActionsJob : public BaseJob { +class QUOTIENT_API GetPushRuleActionsJob : public BaseJob { public: /*! \brief The actions for a push rule * @@ -258,7 +258,7 @@ public: * This endpoint allows clients to change the actions of a push rule. * This can be used to change the actions of builtin rules. */ -class SetPushRuleActionsJob : public BaseJob { +class QUOTIENT_API SetPushRuleActionsJob : public BaseJob { public: /*! \brief Set the actions for a push rule. * diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index 00a2aa0d..d13fa4fc 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -13,7 +13,7 @@ namespace Quotient { * Sets the position of the read marker for a given room, and optionally * the read receipt's location. */ -class SetReadMarkerJob : public BaseJob { +class QUOTIENT_API SetReadMarkerJob : public BaseJob { public: /*! \brief Set the position of the read marker for a room. * diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 7ac093cd..e29e7b29 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -13,7 +13,7 @@ namespace Quotient { * This API updates the marker for the given receipt type to the event ID * specified. */ -class PostReceiptJob : public BaseJob { +class QUOTIENT_API PostReceiptJob : public BaseJob { public: /*! \brief Send a receipt for the given event ID. * diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index f0db9f9f..29d9c5d5 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -22,7 +22,7 @@ namespace Quotient { * * Server administrators may redact events sent by users on their server. */ -class RedactEventJob : public BaseJob { +class QUOTIENT_API RedactEventJob : public BaseJob { public: /*! \brief Strips all non-integrity-critical information out of an event. * diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index c1614f20..10375971 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -59,7 +59,7 @@ namespace Quotient { * Any user ID returned by this API must conform to the grammar given in the * [Matrix specification](/appendices/#user-identifiers). */ -class RegisterJob : public BaseJob { +class QUOTIENT_API RegisterJob : public BaseJob { public: /*! \brief Register for an account on this homeserver. * @@ -143,7 +143,7 @@ public: * should validate the email itself, either by sending a validation email * itself or by using a service it has control over. */ -class RequestTokenToRegisterEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenToRegisterEmailJob : public BaseJob { public: /*! \brief Begins the validation process for an email to be used during * registration. @@ -175,7 +175,7 @@ public: * should validate the phone number itself, either by sending a validation * message itself or by using a service it has control over. */ -class RequestTokenToRegisterMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenToRegisterMSISDNJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given phone number for * the purpose of registering an account @@ -215,7 +215,7 @@ public: * access token provided in the request. Whether other access tokens for * the user are revoked depends on the request parameters. */ -class ChangePasswordJob : public BaseJob { +class QUOTIENT_API ChangePasswordJob : public BaseJob { public: /*! \brief Changes a user's password. * @@ -257,7 +257,7 @@ public: * The homeserver should validate the email itself, either by sending a * validation email itself or by using a service it has control over. */ -class RequestTokenToResetPasswordEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenToResetPasswordEmailJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given email address * for the purpose of resetting a user's password @@ -309,7 +309,7 @@ public: * The homeserver should validate the phone number itself, either by sending a * validation message itself or by using a service it has control over. */ -class RequestTokenToResetPasswordMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenToResetPasswordMSISDNJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given phone number for * the purpose of resetting a user's password. @@ -361,7 +361,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class DeactivateAccountJob : public BaseJob { +class QUOTIENT_API DeactivateAccountJob : public BaseJob { public: /*! \brief Deactivate a user's account. * @@ -411,7 +411,7 @@ public: * reserve the username. This can mean that the username becomes unavailable * between checking its availability and attempting to register it. */ -class CheckUsernameAvailabilityJob : public BaseJob { +class QUOTIENT_API CheckUsernameAvailabilityJob : public BaseJob { public: /*! \brief Checks to see if a username is available on the server. * diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index e401c2e1..8c533c19 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -13,7 +13,7 @@ namespace Quotient { * Reports an event as inappropriate to the server, which may then notify * the appropriate people. */ -class ReportContentJob : public BaseJob { +class QUOTIENT_API ReportContentJob : public BaseJob { public: /*! \brief Reports an event as inappropriate. * diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 96f5beca..fea3d59d 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -18,7 +18,7 @@ namespace Quotient { * fields in this object will vary depending on the type of event. See * [Room Events](/client-server-api/#room-events) for the m. event specification. */ -class SendMessageJob : public BaseJob { +class QUOTIENT_API SendMessageJob : public BaseJob { public: /*! \brief Send a message event to the given room. * diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index f95af223..a00b0947 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -29,7 +29,7 @@ namespace Quotient { * state event is to be sent. Servers do not validate aliases which are * being removed or are already present in the state event. */ -class SetRoomStateWithKeyJob : public BaseJob { +class QUOTIENT_API SetRoomStateWithKeyJob : public BaseJob { public: /*! \brief Send a state event to the given room. * diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h index 58327587..0432f667 100644 --- a/lib/csapi/room_upgrades.h +++ b/lib/csapi/room_upgrades.h @@ -12,7 +12,7 @@ namespace Quotient { * * Upgrades the given room to a particular room version. */ -class UpgradeRoomJob : public BaseJob { +class QUOTIENT_API UpgradeRoomJob : public BaseJob { public: /*! \brief Upgrades a room to a new room version. * diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 2620582b..f0815109 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -15,7 +15,7 @@ namespace Quotient { * Get a single event based on `roomId/eventId`. You must have permission to * retrieve this event e.g. by being a member in the room for this event. */ -class GetOneRoomEventJob : public BaseJob { +class QUOTIENT_API GetOneRoomEventJob : public BaseJob { public: /*! \brief Get a single event by event ID. * @@ -48,7 +48,7 @@ public: * state of the room. If the user has left the room then the state is * taken from the state of the room when they left. */ -class GetRoomStateWithKeyJob : public BaseJob { +class QUOTIENT_API GetRoomStateWithKeyJob : public BaseJob { public: /*! \brief Get the state identified by the type and key. * @@ -80,7 +80,7 @@ public: * * Get the state events for the current state of a room. */ -class GetRoomStateJob : public BaseJob { +class QUOTIENT_API GetRoomStateJob : public BaseJob { public: /*! \brief Get all state events in the current state of a room. * @@ -106,7 +106,7 @@ public: * * Get the list of members for this room. */ -class GetMembersByRoomJob : public BaseJob { +class QUOTIENT_API GetMembersByRoomJob : public BaseJob { public: /*! \brief Get the m.room.member events for the room. * @@ -161,7 +161,7 @@ public: * respond than `/members` as it can be implemented more efficiently on the * server. */ -class GetJoinedMembersByRoomJob : public BaseJob { +class QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 3d02752a..8683413d 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -15,7 +15,7 @@ namespace Quotient { * * Performs a full text search across different categories. */ -class SearchJob : public BaseJob { +class QUOTIENT_API SearchJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h index ade1eb7d..f4f81c1e 100644 --- a/lib/csapi/sso_login_redirect.h +++ b/lib/csapi/sso_login_redirect.h @@ -17,7 +17,7 @@ namespace Quotient { * or present a page which lets the user select an IdP to continue * with in the event multiple are supported by the server. */ -class RedirectToSSOJob : public BaseJob { +class QUOTIENT_API RedirectToSSOJob : public BaseJob { public: /*! \brief Redirect the user's browser to the SSO interface. * @@ -44,7 +44,7 @@ public: * The server MUST respond with an HTTP redirect to the SSO interface * for that IdP. */ -class RedirectToIdPJob : public BaseJob { +class QUOTIENT_API RedirectToIdPJob : public BaseJob { public: /*! \brief Redirect the user's browser to the SSO interface for an IdP. * diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index a854531a..f4250674 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -12,7 +12,7 @@ namespace Quotient { * * List the tags set by a user on a room. */ -class GetRoomTagsJob : public BaseJob { +class QUOTIENT_API GetRoomTagsJob : public BaseJob { public: // Inner data structures @@ -68,7 +68,7 @@ struct JsonObjectConverter { * * Add a tag to the room. */ -class SetRoomTagJob : public BaseJob { +class QUOTIENT_API SetRoomTagJob : public BaseJob { public: /*! \brief Add a tag to a room. * @@ -98,7 +98,7 @@ public: * * Remove a tag from the room. */ -class DeleteRoomTagJob : public BaseJob { +class QUOTIENT_API DeleteRoomTagJob : public BaseJob { public: /*! \brief Remove a tag from the room. * diff --git a/lib/csapi/third_party_lookup.h b/lib/csapi/third_party_lookup.h index 969e767c..30c5346e 100644 --- a/lib/csapi/third_party_lookup.h +++ b/lib/csapi/third_party_lookup.h @@ -18,7 +18,7 @@ namespace Quotient { * homeserver. Includes both the available protocols and all fields * required for queries against each protocol. */ -class GetProtocolsJob : public BaseJob { +class QUOTIENT_API GetProtocolsJob : public BaseJob { public: /// Retrieve metadata about all protocols that a homeserver supports. explicit GetProtocolsJob(); @@ -45,7 +45,7 @@ public: * Fetches the metadata from the homeserver about a particular third party * protocol. */ -class GetProtocolMetadataJob : public BaseJob { +class QUOTIENT_API GetProtocolMetadataJob : public BaseJob { public: /*! \brief Retrieve metadata about a specific protocol that the homeserver * supports. @@ -82,7 +82,7 @@ public: * identifier. It should attempt to canonicalise the identifier as much * as reasonably possible given the network type. */ -class QueryLocationByProtocolJob : public BaseJob { +class QUOTIENT_API QueryLocationByProtocolJob : public BaseJob { public: /*! \brief Retrieve Matrix-side portals rooms leading to a third party * location. @@ -119,7 +119,7 @@ public: * Retrieve a Matrix User ID linked to a user on the third party service, given * a set of user parameters. */ -class QueryUserByProtocolJob : public BaseJob { +class QUOTIENT_API QueryUserByProtocolJob : public BaseJob { public: /*! \brief Retrieve the Matrix User ID of a corresponding third party user. * @@ -155,7 +155,7 @@ public: * Retrieve an array of third party network locations from a Matrix room * alias. */ -class QueryLocationByAliasJob : public BaseJob { +class QUOTIENT_API QueryLocationByAliasJob : public BaseJob { public: /*! \brief Reverse-lookup third party locations given a Matrix room alias. * @@ -184,7 +184,7 @@ public: * * Retrieve an array of third party users from a Matrix User ID. */ -class QueryUserByIDJob : public BaseJob { +class QUOTIENT_API QueryUserByIDJob : public BaseJob { public: /*! \brief Reverse-lookup third party users given a Matrix User ID. * diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index a424678f..1edb969e 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -52,7 +52,7 @@ namespace Quotient { * If a token is requested from the identity server, the homeserver will * append a `m.room.third_party_invite` event to the room. */ -class InviteBy3PIDJob : public BaseJob { +class QUOTIENT_API InviteBy3PIDJob : public BaseJob { public: /*! \brief Invite a user to participate in a particular room. * diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index 7a237195..5b6e0bfb 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -13,7 +13,7 @@ namespace Quotient { * This endpoint is used to send send-to-device events to a set of * client devices. */ -class SendToDeviceJob : public BaseJob { +class QUOTIENT_API SendToDeviceJob : public BaseJob { public: /*! \brief Send an event to a given set of devices. * diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index 64a310d0..234e91b0 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -15,7 +15,7 @@ namespace Quotient { * Alternatively, if `typing` is `false`, it tells the server that the * user has stopped typing. */ -class SetTypingJob : public BaseJob { +class QUOTIENT_API SetTypingJob : public BaseJob { public: /*! \brief Informs the server that the user has started or stopped typing. * diff --git a/lib/csapi/users.h b/lib/csapi/users.h index ec186592..3c99758b 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -21,7 +21,7 @@ namespace Quotient { * names preferably using a collation determined based upon the * `Accept-Language` header provided in the request, if present. */ -class SearchUserDirectoryJob : public BaseJob { +class QUOTIENT_API SearchUserDirectoryJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 896e2ea9..4445dbd2 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -31,7 +31,7 @@ namespace Quotient { * upgrade appropriately. Additionally, clients should avoid using unstable * features in their stable releases. */ -class GetVersionsJob : public BaseJob { +class QUOTIENT_API GetVersionsJob : public BaseJob { public: /// Gets the versions of the specification supported by the server. explicit GetVersionsJob(); diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index 087ebbbd..38904f60 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -13,7 +13,7 @@ namespace Quotient { * This API provides credentials for the client to use when initiating * calls. */ -class GetTurnServerJob : public BaseJob { +class QUOTIENT_API GetTurnServerJob : public BaseJob { public: /// Obtain TURN server credentials. explicit GetTurnServerJob(); diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index c707d232..8615191c 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -21,7 +21,7 @@ namespace Quotient { * Note that this endpoint is not necessarily handled by the homeserver, * but by another webserver, to be used for discovering the homeserver URL. */ -class GetWellknownJob : public BaseJob { +class QUOTIENT_API GetWellknownJob : public BaseJob { public: /// Gets Matrix server discovery information about the domain. explicit GetWellknownJob(); diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index 319f82c5..fba099f6 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -19,7 +19,7 @@ namespace Quotient { * is registered by the appservice, and return it in the response * body. */ -class GetTokenOwnerJob : public BaseJob { +class QUOTIENT_API GetTokenOwnerJob : public BaseJob { public: /// Gets information about the owner of an access token. explicit GetTokenOwnerJob(); -- cgit v1.2.3 From 7eda212753057c07f429dfdfb0cf3a18312de054 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Dec 2021 21:46:57 +0100 Subject: Refactor EventFactory and move it out of _impl:: Strictly speaking, EventFactory can be further instantiated if any client application figures they need a whole new base class for events and respectively a separate EventFactory specialisation for it. Where this whole commit started though was a linkage error because I did not plan to expose Quotient-specific logging categories for linkage (effectively, usage) from the client code - meanwhile the inline code of EventFactory uses qDebug(EVENTS), meaning I had to either add QUOTIENT_API to EVENTS or hide those invocations. This in turn led to trimming the EventFactory constructor back to trivial implementation and dropping the guard variable that was supposed to trace duplicate EventFactory objects for the same BaseEventT - with the reasoning that such situation is not really dangerous (unlike EventTypeRegistry double-initialisation fiasco, see #413), and at the same time it can be easily detected in the logs by duplicated factory method registration messages. And while I was at it, I replaced the meaningless bool in the return type of EventFactory<>::addMethod with the slightly more (but still barely) useful reference to the inserted factory method. One can (in theory) use it now if they need to turn some event JSON into an object of some specific event type or nullptr if the event type in the JSON payload doesn't match - but at the same rate (for now at least) one can call makeIfMatches() directly. With this commit, both Quotest and Quaternion build and link using either Clang or GCC even under -fvisibility=hidden. However, running quotest now reproduces #413, which is a matter of event typeId infrastructure refactoring, coming in further commits. --- lib/events/event.cpp | 8 +++ lib/events/event.h | 159 ++++++++++++++++++++++++------------------------ lib/events/roomevent.h | 2 +- lib/events/stateevent.h | 2 +- 4 files changed, 88 insertions(+), 83 deletions(-) diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 96be717c..715e7da2 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -27,6 +27,14 @@ QString EventTypeRegistry::getMatrixType(event_type_t typeId) : QString(); } +void _impl::EventFactoryBase::logAddingMethod(event_mtype_t matrixType, + size_t newSize) +{ + qDebug(EVENTS) << "Adding factory method for" << matrixType << "events;" + << newSize << "methods will be in the" << name + << "chain"; +} + Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) { if (!json.contains(ContentKeyL) diff --git a/lib/events/event.h b/lib/events/event.h index 8a0076d0..0aef49f7 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -120,80 +120,93 @@ inline event_ptr_tt makeEvent(ArgTs&&... args) } namespace _impl { - template - event_ptr_tt makeIfMatches(const QJsonObject& json, - const QString& matrixType) + class QUOTIENT_API EventFactoryBase { + public: + EventFactoryBase(const EventFactoryBase&) = delete; + + protected: // This class is only to inherit from + explicit EventFactoryBase(const char* name) + : name(name) + {} + void logAddingMethod(event_mtype_t mtypeId, size_t newSize); + + private: + const char* const name; + }; +} // namespace _impl + +//! \brief A family of event factories to create events from CS API responses +//! +//! Each of these factories, as instantiated by event base types (Event, +//! RoomEvent etc.) is capable of producing an event object derived from +//! \p BaseEventT, using the JSON payload and the event type passed to its +//! make() method. Don't use these directly to make events; use loadEvent() +//! overloads as the frontend for these. Never instantiate new factories +//! outside of base event classes. +//! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory, +//! StateEventBase::factory +template +class EventFactory : public _impl::EventFactoryBase { +private: + std::vector (*)(const QJsonObject&, const QString&)> + methods {}; + + template + static event_ptr_tt makeIfMatches(const QJsonObject& json, + const QString& matrixType) { return QLatin1String(EventT::matrixTypeId()) == matrixType ? makeEvent(json) : nullptr; } - //! \brief A family of event factories to create events from CS API responses +public: + explicit EventFactory(const char* fName) + : EventFactoryBase { fName } + {} + + //! \brief Add a method to create events of a given type //! - //! Each of these factories, as instantiated by event base types (Event, - //! RoomEvent etc.) is capable of producing an event object derived from - //! \p BaseEventT, using the JSON payload and the event type passed to its - //! make() method. Don't use these directly to make events; use loadEvent() - //! overloads as the frontend for these. Never instantiate new factories - //! outside of base event classes. - //! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory, - //! StateEventBase::factory - template - class EventFactory - : private std::vector (*)(const QJsonObject&, - const QString&)> { - // Actual makeIfMatches specialisations will differ in the first - // template parameter but that doesn't affect the function type - public: - explicit EventFactory(const char* name = "") - : name(name) - { - static auto yetToBeConstructed = true; - Q_ASSERT(yetToBeConstructed); - if (!yetToBeConstructed) // For Release builds that pass Q_ASSERT - qCritical(EVENTS) - << "Another EventFactory for the same base type is being " - "created - event creation logic will be splintered"; - yetToBeConstructed = false; - } - EventFactory(const EventFactory&) = delete; - - //! \brief Add a method to create events of a given type - //! - //! Adds a standard factory method (makeIfMatches) for \p EventT so that - //! event objects of this type can be created dynamically by loadEvent. - //! The caller is responsible for ensuring this method is called only - //! once per type. - //! \sa makeIfMatches, loadEvent, Quotient::loadEvent - template - bool addMethod() - { - this->emplace_back(&makeIfMatches); - qDebug(EVENTS) << "Added factory method for" - << EventT::matrixTypeId() << "events;" << this->size() - << "methods in the" << name << "chain by now"; - return true; - } - - auto loadEvent(const QJsonObject& json, const QString& matrixType) - { - for (const auto& f : *this) - if (auto e = f(json, matrixType)) - return e; - return makeEvent(unknownEventTypeId(), json); - } + //! Adds a standard factory method (makeIfMatches) for \p EventT so that + //! event objects of this type can be created dynamically by loadEvent. + //! The caller is responsible for ensuring this method is called only + //! once per type. + //! \sa loadEvent, Quotient::loadEvent + template + const auto& addMethod() + { + logAddingMethod(EventT::matrixTypeId(), methods.size() + 1); + return methods.emplace_back(&makeIfMatches); + } - const char* const name; - }; -} // namespace _impl + auto loadEvent(const QJsonObject& json, const QString& matrixType) + { + for (const auto& f : methods) + if (auto e = f(json, matrixType)) + return e; + return makeEvent(unknownEventTypeId(), json); + } +}; + +//! \brief Point of customisation to dynamically load events +//! +//! The default specialisation of this calls BaseEventT::factory.loadEvent() +//! and if that fails (i.e. returns nullptr) creates an unknown event of +//! BaseEventT. Other specialisations may reuse other factories, add validations +//! common to BaseEventT events, and so on. +template +event_ptr_tt doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + return BaseEventT::factory.loadEvent(json, matrixType); +} // === Event === class QUOTIENT_API Event { public: using Type = event_type_t; - static inline _impl::EventFactory factory { "Event" }; + static inline EventFactory factory { "Event" }; explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, @@ -273,37 +286,21 @@ using Events = EventsArray; // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). #define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static QUOTIENT_EXPORT constexpr event_mtype_t matrixTypeId() \ + static QUOTIENT_API constexpr event_mtype_t matrixTypeId() \ { \ return _Id; \ } \ - static QUOTIENT_EXPORT auto typeId() { return Quotient::typeId<_Type>(); } \ + static QUOTIENT_API auto typeId() { return Quotient::typeId<_Type>(); } \ // End of macro // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphic event arrays -#define REGISTER_EVENT_TYPE(_Type) \ - [[maybe_unused]] QUOTIENT_API inline const auto _factoryAdded##_Type = \ - _Type::factory.addMethod<_Type>(); \ +#define REGISTER_EVENT_TYPE(Type_) \ + [[maybe_unused]] QUOTIENT_API inline const auto& factoryMethodFor##Type_ = \ + Type_::factory.addMethod(); \ // End of macro -// === Event loading === -// (see also event_loader.h) - -//! \brief Point of customisation to dynamically load events -//! -//! The default specialisation of this calls BaseEventT::factory and if that -//! fails (i.e. returns nullptr) creates an unknown event of BaseEventT. -//! Other specialisations may reuse other factories, add validations common to -//! BaseEventT, and so on -template -event_ptr_tt doLoadEvent(const QJsonObject& json, - const QString& matrixType) -{ - return BaseEventT::factory.loadEvent(json, matrixType); -} - // === is<>(), eventCast<>() and switchOnType<>() === template diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 3fbb247e..dcee1170 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -13,7 +13,7 @@ class RedactionEvent; /** This class corresponds to m.room.* events */ class QUOTIENT_API RoomEvent : public Event { public: - static inline _impl::EventFactory factory { "RoomEvent" }; + static inline EventFactory factory { "RoomEvent" }; // RedactionEvent is an incomplete type here so we cannot inline // constructors and destructors and we cannot use 'using'. diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 6095d628..88da68f8 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -19,7 +19,7 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, class QUOTIENT_API StateEventBase : public RoomEvent { public: - static inline _impl::EventFactory factory { "StateEvent" }; + static inline EventFactory factory { "StateEvent" }; StateEventBase(Type type, const QJsonObject& json); StateEventBase(Type type, event_mtype_t matrixType, -- cgit v1.2.3 From 806bfd6da07127c33d1014ef2335a8b9602afe6c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 29 Dec 2021 18:17:38 +0100 Subject: CMakeLists.txt: apply -fvisibility=hidden Also, -fvisibility-inlines-hidden is applied in a CMake-native way now. As can be expected, BUILDING_SHARED_QUOTIENT is set when a dynamic library is built while QUOTIENT_STATIC is set whenever static libQuotient is around (both for building it and for building with it). --- CMakeLists.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fc47b42..45aa3726 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ if (MSVC) /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() foreach (FLAG Wall Wpedantic Wextra Werror=return-type Wno-unused-parameter - Wno-gnu-zero-variadic-macro-arguments fvisibility-inlines-hidden) + Wno-gnu-zero-variadic-macro-arguments) CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) if ( COMPILER_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") @@ -254,6 +254,12 @@ file(GLOB_RECURSE api_ALL_SRCS CONFIGURE_DEPENDS ${FULL_CSAPI_DIR}/*.* lib/${ASAPI_DEF_DIR}/*.* lib/${ISAPI_DEF_DIR}/*.*) add_library(${PROJECT_NAME} ${lib_SRCS} ${api_ALL_SRCS}) +# Set BUILDING_SHARED_QUOTIENT if building as a shared library +target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$,SHARED_LIBRARY>:BUILDING_SHARED_QUOTIENT>) +# Set QUOTIENT_STATIC in a static library setting +target_compile_definitions(${PROJECT_NAME} PUBLIC + $<$,STATIC_LIBRARY>:QUOTIENT_STATIC>) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} @@ -265,6 +271,8 @@ endif() set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20 CXX_EXTENSIONS OFF + VISIBILITY_INLINES_HIDDEN ON + CXX_VISIBILITY_PRESET hidden VERSION "${PROJECT_VERSION}" SOVERSION ${API_VERSION} INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${API_VERSION} -- cgit v1.2.3 From ac7ee73cb4fa1ac05f7bcae1800cb79b36c7647f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Dec 2021 20:20:41 +0100 Subject: CI: Put the shared object configuration to the test For now on Linux with GCC only, with a plan to add Windows eventually. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1b6f0c0..e4bf8e29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,8 +105,10 @@ jobs: fi echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV + # Build libQuotient as a shared library for the sonar-enabled run + # to have this configuration also covered CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DBUILD_SHARED_LIBS=false \ + -DBUILD_SHARED_LIBS=${{ matrix.sonar }} \ -DCMAKE_INSTALL_PREFIX=~/.local \ -DCMAKE_PREFIX_PATH=~/.local \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" -- cgit v1.2.3 From 27bb7ba696ae803c6a6903f85fe14074b23b7bcc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 28 Dec 2021 21:34:03 +0100 Subject: Use QLatin1String for event typeId's Before all, this fixes the problem with double-initialising of type ids; it could have been fixed with a smaller change but EventTypeRegistry is fairly superfluous now when inline variables are a thing and it's possible to have an extensible registry system using literally pointers to the memory that are guaranteed to be unique. That being said, event_type_t is still QLatin1String and not a bare const char* (or void*), mostly to stay on the safe side when it comes to type identities: unlike const char*, QLatin1String's are deep-compared, meaning that matching for switchOnType (former visit) occurs a bit slower now. This may change in the future; but this is the first step in getting rid of EventTypeRegistry. This change means that initializeTypeId is no more needed; also, two static member functions, typeId() and matrixTypeId(), are being replaced with a single inline static member variable, TypeId. This commit doesn't apply that transition across the event types, meaning that you'll get a pile of warnings when compiling the library. These warnings will be tackled in further commits within this branch. --- lib/events/event.cpp | 22 +++-------------- lib/events/event.h | 68 ++++++++++++++++------------------------------------ 2 files changed, 23 insertions(+), 67 deletions(-) diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 715e7da2..4c304a3c 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -9,28 +9,12 @@ using namespace Quotient; -event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) -{ - const auto id = get().eventTypes.size(); - get().eventTypes.push_back(matrixTypeId); - if (strncmp(matrixTypeId, "", 1) == 0) - qDebug(EVENTS) << "Initialized unknown event type with id" << id; - else - qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" - << id; - return id; -} - -QString EventTypeRegistry::getMatrixType(event_type_t typeId) -{ - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] - : QString(); -} +QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId; } -void _impl::EventFactoryBase::logAddingMethod(event_mtype_t matrixType, +void _impl::EventFactoryBase::logAddingMethod(event_type_t TypeId, size_t newSize) { - qDebug(EVENTS) << "Adding factory method for" << matrixType << "events;" + qDebug(EVENTS) << "Adding factory method for" << TypeId << "events;" << newSize << "methods will be in the" << name << "chain"; } diff --git a/lib/events/event.h b/lib/events/event.h index 0aef49f7..47f07c1d 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -55,60 +55,32 @@ inline QJsonObject basicEventJson(const QString& matrixType, return { { TypeKey, matrixType }, { ContentKey, content } }; } -// === Event types and event types registry === +// === Event types === -using event_type_t = size_t; +using event_type_t = QLatin1String; using event_mtype_t = const char*; class QUOTIENT_API EventTypeRegistry { public: ~EventTypeRegistry() = default; - static event_type_t initializeTypeId(event_mtype_t matrixTypeId); - - template - static event_type_t initializeTypeId() - { - return initializeTypeId(EventT::matrixTypeId()); - } - + [[deprecated("event_type_t is a string now, use it directly instead")]] static QString getMatrixType(event_type_t typeId); private: EventTypeRegistry() = default; Q_DISABLE_COPY_MOVE(EventTypeRegistry) - - static EventTypeRegistry& get() - { - static EventTypeRegistry etr; - return etr; - } - - std::vector eventTypes; -}; - -template <> -inline event_type_t EventTypeRegistry::initializeTypeId() -{ - return initializeTypeId(""); -} - -template -struct EventTypeTraits { - static event_type_t id() - { - static const auto id = EventTypeRegistry::initializeTypeId(); - return id; - } }; template inline event_type_t typeId() { - return EventTypeTraits>::id(); + return std::decay_t::TypeId; } -inline event_type_t unknownEventTypeId() { return typeId(); } +constexpr inline event_type_t UnknownEventTypeId = "?"_ls; +[[deprecated("Use UnknownEventTypeId")]] +constexpr inline event_type_t unknownEventTypeId() { return UnknownEventTypeId; } // === Event creation facilities === @@ -128,7 +100,7 @@ namespace _impl { explicit EventFactoryBase(const char* name) : name(name) {} - void logAddingMethod(event_mtype_t mtypeId, size_t newSize); + void logAddingMethod(event_type_t TypeId, size_t newSize); private: const char* const name; @@ -155,9 +127,9 @@ private: static event_ptr_tt makeIfMatches(const QJsonObject& json, const QString& matrixType) { - return QLatin1String(EventT::matrixTypeId()) == matrixType - ? makeEvent(json) - : nullptr; + // If your matrix event type is not all ASCII, it's your problem + // (see https://github.com/matrix-org/matrix-doc/pull/2758) + return EventT::TypeId == matrixType ? makeEvent(json) : nullptr; } public: @@ -175,7 +147,7 @@ public: template const auto& addMethod() { - logAddingMethod(EventT::matrixTypeId(), methods.size() + 1); + logAddingMethod(EventT::TypeId, methods.size() + 1); return methods.emplace_back(&makeIfMatches); } @@ -184,7 +156,7 @@ public: for (const auto& f : methods) if (auto e = f(json, matrixType)) return e; - return makeEvent(unknownEventTypeId(), json); + return makeEvent(UnknownEventTypeId, json); } }; @@ -285,12 +257,12 @@ using Events = EventsArray; // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static QUOTIENT_API constexpr event_mtype_t matrixTypeId() \ - { \ - return _Id; \ - } \ - static QUOTIENT_API auto typeId() { return Quotient::typeId<_Type>(); } \ +#define DEFINE_EVENT_TYPEID(Id_, Type_) \ + static inline constexpr event_type_t TypeId = Id_##_ls; \ + [[deprecated("Use _Type::TypeId directly instead")]] \ + static constexpr event_mtype_t matrixTypeId() { return Id_; } \ + [[deprecated("Use _Type::TypeId directly instead")]] \ + static event_type_t typeId() { return TypeId; } \ // End of macro // This macro should be put after an event class definition (in .h or .cpp) @@ -311,7 +283,7 @@ inline bool is(const Event& e) inline bool isUnknown(const Event& e) { - return e.type() == unknownEventTypeId(); + return e.type() == UnknownEventTypeId; } template -- 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(-) 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 cae9d908e8b319e8a0c33f7089ecb7f2e9317de8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Jan 2022 13:40:09 +0100 Subject: CI: up to Qt 5.12.12 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1b6f0c0..5c2c820e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: matrix: os: [ubuntu-20.04, macos-10.15] compiler: [ GCC, Clang ] - qt-version: [ '5.12.10' ] + qt-version: [ '5.12.12' ] # Not using binary values here, to make the job captions more readable e2ee: [ '' ] update-api: [ '', 'update-api' ] @@ -34,17 +34,17 @@ jobs: include: - os: ubuntu-latest compiler: GCC - qt-version: '5.12.10' + qt-version: '5.12.12' sonar: 'sonar' - os: windows-2019 compiler: MSVC platform: x64 - qt-version: '5.12.10' + qt-version: '5.12.12' qt-arch: win64_msvc2017_64 - os: windows-2019 compiler: MSVC platform: x64 - qt-version: '5.12.10' + qt-version: '5.12.12' qt-arch: win64_msvc2017_64 update-api: update-api -- cgit v1.2.3 From e4511d02a20e53e1aa2e5d4886bd68dd73f885b3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Jan 2022 19:21:59 +0100 Subject: Get LGTM working again Unfortunately LGTM still sits on Ubuntu 19.10 (not even LTS) which really limits the choice of compilers: the newest GCC is of version 9 and the newest Clang seems to be version 9 as well - both are quite old and giving when it comes to modern features support. The latest error is GCC 9 tripping over the assignment of the function specialisation to std::function. Clang 9 doesn't and otherwise seems fine. --- .lgtm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lgtm.yml b/.lgtm.yml index 308675a8..a01e1de9 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -15,4 +15,4 @@ extraction: # - cmake --build build # - popd configure: - command: "cmake . -GNinja" # -DOlm_DIR=olm/build" + command: "CXX=clang++-9 cmake . -GNinja" # -DOlm_DIR=olm/build" -- cgit v1.2.3 From 545f9084bacead9f02ba6df609822be4672b7952 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Jan 2022 20:02:05 +0100 Subject: CI: add events.debug to QT_LOGGING_RULES --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c2c820e..e56ea26e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,7 +198,7 @@ jobs: env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' + QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true;quotient.events.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | ctest --test-dir $BUILD_PATH --output-on-failure -- cgit v1.2.3 From bbaaab6590e5435fd9c967ca4af5ef06d9182698 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Jan 2022 20:41:04 +0100 Subject: BUILD_SHARED_LIBS in most jobs; fix not finding libQuotient.so.* --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4bf8e29..d4af9b30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,10 +105,10 @@ jobs: fi echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - # Build libQuotient as a shared library for the sonar-enabled run - # to have this configuration also covered + # Build libQuotient as a shared library across platforms but also + # check the static configuration somewhere CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DBUILD_SHARED_LIBS=${{ matrix.sonar }} \ + -DBUILD_SHARED_LIBS=${{ !matrix.sonar }} \ -DCMAKE_INSTALL_PREFIX=~/.local \ -DCMAKE_PREFIX_PATH=~/.local \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" @@ -122,6 +122,7 @@ jobs: if [[ '${{ runner.os }}' != 'Windows' ]]; then BIN_DIR=/bin + echo "LIB_PATH=$HOME/.local/lib" >>$GITHUB_ENV fi echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV echo "~/.local$BIN_DIR" >>$GITHUB_PATH @@ -205,6 +206,7 @@ jobs: run: | ctest --test-dir $BUILD_PATH --output-on-failure [[ -z "$TEST_USER" ]] || \ + LD_LIBRARY_PATH=$LIB_PATH \ $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually -- cgit v1.2.3 From 39b50a13a0379ea32be114c85f3c697d75d4e03b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 29 Dec 2021 21:22:26 +0100 Subject: Quotest: build with hidden visibility too --- quotest/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index cb41141d..ec305620 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -8,6 +8,11 @@ find_package(${Qt} COMPONENTS Concurrent) add_executable(quotest ${quotest_SRCS}) target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME}) +set_target_properties(quotest PROPERTIES + VISIBILITY_INLINES_HIDDEN ON + CXX_VISIBILITY_PRESET hidden +) + if (MSVC) target_compile_options(quotest PUBLIC /EHsc /W4 /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 -- cgit v1.2.3 From 5b8ea1a0d419fda1729aaa81e34ad20e0dacef44 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 30 Dec 2021 10:40:07 +0100 Subject: Try to fix building with MSVC with Qt pre-5.14 MSVC is quite picky to redeclaration with __declspec(dllexport), judging it as changing the class of storage. This commit tries to reorder declarations so that MSVC is made aware of dllexport attribute on the first encounter rather than the second one. --- lib/quotient_common.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index a5926e8c..3d38ce1f 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -29,13 +29,13 @@ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -// The first line is a usual way to indicate a namespace to moc; -// the second line redeclares the namespace static metaobject with -// QUOTIENT_API so that dynamically linked clients could serialise -// flag/enum values from the namespace. +// The first line forward-declares the namespace static metaobject with +// QUOTIENT_API so that dynamically linked clients could serialise flag/enum +// values from the namespace; Qt before 5.14 doesn't help with that. The second +// line is needed for moc to do its job on the namespace. #define QUO_NAMESPACE \ -Q_NAMESPACE \ -extern QUOTIENT_API const QMetaObject staticMetaObject; + extern QUOTIENT_API const QMetaObject staticMetaObject; \ + Q_NAMESPACE #else // Since Qt 5.14.0, it's all packed in a single macro #define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API) -- cgit v1.2.3 From a5a261b2c0dc60f99c8caa4729683d2780677f88 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 30 Dec 2021 12:46:50 +0100 Subject: Define destructors out-of-line when unique/scoped ptr are involved Once visibility kicks in, MSVC changes its ways and tries to instantiate Private classes wrapped in smart pointers upon their occurence in the header file - which leads to build breakage because of a missing destructor. Usually making the outer class destructor out-of-line helps to fix this (see RoomEvent, for one example). --- lib/jobs/downloadfilejob.cpp | 2 ++ lib/jobs/downloadfilejob.h | 1 + lib/mxcreply.cpp | 2 ++ lib/mxcreply.h | 2 ++ 4 files changed, 7 insertions(+) diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 0b0531ad..6bf221cb 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -37,6 +37,8 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, setObjectName(QStringLiteral("DownloadFileJob")); } +DownloadFileJob::~DownloadFileJob() = default; + QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index d9f3b686..9e807fe7 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -13,6 +13,7 @@ public: DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename = {}); + ~DownloadFileJob() override; QString targetFileName() const; diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 0b6643fc..fb16c79f 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -38,6 +38,8 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) }); } +MxcReply::~MxcReply() = default; + #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #define ERROR_SIGNAL errorOccurred #else diff --git a/lib/mxcreply.h b/lib/mxcreply.h index 23049b7d..1d31d608 100644 --- a/lib/mxcreply.h +++ b/lib/mxcreply.h @@ -13,10 +13,12 @@ class Room; class QUOTIENT_API MxcReply : public QNetworkReply { + Q_OBJECT public: explicit MxcReply(); explicit MxcReply(QNetworkReply *reply); MxcReply(QNetworkReply* reply, Room* room, const QString& eventId); + ~MxcReply() override; public Q_SLOTS: void abort() override; -- cgit v1.2.3 From 8f03628ee0e4d1d1cb4e2f237e8fa695bc2cde42 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 30 Dec 2021 15:24:58 +0100 Subject: Drop inline next to constexpr Thanks to Sonar for reminding that constexpr implies inline. --- lib/events/event.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 47f07c1d..692e88e7 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -78,9 +78,9 @@ inline event_type_t typeId() return std::decay_t::TypeId; } -constexpr inline event_type_t UnknownEventTypeId = "?"_ls; +constexpr event_type_t UnknownEventTypeId = "?"_ls; [[deprecated("Use UnknownEventTypeId")]] -constexpr inline event_type_t unknownEventTypeId() { return UnknownEventTypeId; } +constexpr event_type_t unknownEventTypeId() { return UnknownEventTypeId; } // === Event creation facilities === @@ -258,7 +258,7 @@ using Events = EventsArray; // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). #define DEFINE_EVENT_TYPEID(Id_, Type_) \ - static inline constexpr event_type_t TypeId = Id_##_ls; \ + static constexpr event_type_t TypeId = Id_##_ls; \ [[deprecated("Use _Type::TypeId directly instead")]] \ static constexpr event_mtype_t matrixTypeId() { return Id_; } \ [[deprecated("Use _Type::TypeId directly instead")]] \ -- cgit v1.2.3 From 22ac47b275c2bcad5b5ff3c0cc3e10f3caaeb65b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 30 Dec 2021 15:46:11 +0100 Subject: Don't use QUOTIENT_API inside REGISTER_EVENT_TYPE On Windows QUOTIENT_API expands to different things depending on whether the library is built or used. This results in confusing statements (and MSVC erroring out on them, in some cases - see below - quite legitimately) not only when the application includes Quotient headers but also when the application defines custom events and uses REGISTER_EVENT_TYPE to make them creatable from /sync responses. To avoid repeated registration when dynamic linking is involved, EventFactory<>::addMethod() now bluntly looks up the method for this type in the vector of already registered methods. It would surely be quicker to use a static variable instead; but since the refreshed API for addMethod returns a reference to the factory method it's necessary to do this lookup anyway. Once the primary goal of this branch is achieved across platforms I might experiment with lighter ways to register factory methods; for now here's a minimal change to make the code build on Windows. --- lib/events/event.h | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 692e88e7..4024c6f8 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -73,7 +73,7 @@ private: }; template -inline event_type_t typeId() +constexpr event_type_t typeId() { return std::decay_t::TypeId; } @@ -147,8 +147,12 @@ public: template const auto& addMethod() { + const auto m = &makeIfMatches; + const auto it = std::find(methods.cbegin(), methods.cend(), m); + if (it != methods.cend()) + return *it; logAddingMethod(EventT::TypeId, methods.size() + 1); - return methods.emplace_back(&makeIfMatches); + return methods.emplace_back(m); } auto loadEvent(const QJsonObject& json, const QString& matrixType) @@ -268,9 +272,9 @@ using Events = EventsArray; // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphic event arrays -#define REGISTER_EVENT_TYPE(Type_) \ - [[maybe_unused]] QUOTIENT_API inline const auto& factoryMethodFor##Type_ = \ - Type_::factory.addMethod(); \ +#define REGISTER_EVENT_TYPE(Type_) \ + [[maybe_unused]] inline const auto& factoryMethodFor##Type_ = \ + Type_::factory.addMethod(); \ // End of macro // === is<>(), eventCast<>() and switchOnType<>() === -- cgit v1.2.3 From 874ea3fae21d6b5cab12c8e524e8b25442e4cdd5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 30 Dec 2021 17:55:23 +0100 Subject: Fix more Sonar warnings --- lib/events/event.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 4024c6f8..f12e525e 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -120,8 +120,9 @@ namespace _impl { template class EventFactory : public _impl::EventFactoryBase { private: - std::vector (*)(const QJsonObject&, const QString&)> - methods {}; + using method_t = event_ptr_tt (*)(const QJsonObject&, + const QString&); + std::vector methods {}; template static event_ptr_tt makeIfMatches(const QJsonObject& json, -- cgit v1.2.3 From ca42b3659e1916d384d092b8c31c49e3ffd6441b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 30 Dec 2021 17:56:52 +0100 Subject: CMakeLists: Drop unneeded parts from install(TARGETS) Those DESTINATION specifications match precisely what CMake does by default (on Linux at least); what's worse is that they prevent CMake to install the DLL file on Windows. --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 45aa3726..c889cf13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,8 +316,6 @@ endif() # Configure installation install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} ) install(DIRECTORY lib/ DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} -- cgit v1.2.3 From 2dee2bf4f0b2bd6615866644b2df9460da6babbb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Jan 2022 20:46:12 +0100 Subject: Only test dynamic linking on Linux On Windows and macOS, the assumed practice is to package things up in a monolithic distribution package. On Linux, fine-grained library packages are installed. After adding CMAKE_INSTALL_RPATH_USE_LINK_PATH dynamic linkage is testable on macOS but that's not the "proper" way to install things on the platform, anyway. It also probably works on Windows but I couldn't get a workable setup for quotest after a few attempts. Perhaps it's a matter of running windeployqt instead of trying to figure out and add to PATH all the locations needed; or maybe not; I don't have the motivation to explore this further. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4af9b30..ef6ea3d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: # Build libQuotient as a shared library across platforms but also # check the static configuration somewhere CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DBUILD_SHARED_LIBS=${{ !matrix.sonar }} \ + -DBUILD_SHARED_LIBS=${{ !matrix.sonar && runner.os == 'Linux' }} \ -DCMAKE_INSTALL_PREFIX=~/.local \ -DCMAKE_PREFIX_PATH=~/.local \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" -- cgit v1.2.3 From ca6a104941b71e7b6a8bdcb6ebcdfff5ec8e8aca Mon Sep 17 00:00:00 2001 From: Vladyslav Hnatiuk Date: Sat, 1 Jan 2022 22:14:01 +0100 Subject: Fix enum in room Notification struct --- lib/room.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.h b/lib/room.h index cbe3d1ad..a5513727 100644 --- a/lib/room.h +++ b/lib/room.h @@ -101,7 +101,7 @@ struct EventStats; struct Notification { enum Type { None = 0, Basic, Highlight }; - Q_ENUM(Notification) + Q_ENUM(Type) Type type = None; -- cgit v1.2.3 From 7d37d296f942ac993d041b4576ed52265170c4a8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 2 Jan 2022 06:03:26 +0100 Subject: Add ImplPtr and makeImpl The original (more complex and comprehensive) solution belongs to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html - this commit only provides a small wrapper for non-copyable Private class implementations common throughout libQuotient. Unlike the original, default initialisation is made explicit - you have to pass ZeroImpl() instead (and I firmly believe it's a good thing: normally pointers to Private should not remain nullptr). The reason ZeroImpl<> is not a template variable is quite simple: unique_ptr is non-copyable and so cannot be initialised from; while a template function will initialise the value in-place thanks to copy elision. --- lib/avatar.cpp | 12 ++++-------- lib/avatar.h | 8 ++------ lib/connection.cpp | 3 ++- lib/connection.h | 2 +- lib/connectiondata.cpp | 2 +- lib/connectiondata.h | 5 +++-- lib/jobs/basejob.cpp | 4 ++-- lib/jobs/basejob.h | 2 +- lib/jobs/downloadfilejob.cpp | 5 ++--- lib/jobs/downloadfilejob.h | 3 +-- lib/mxcreply.cpp | 7 +++---- lib/mxcreply.h | 6 ++---- lib/networkaccessmanager.cpp | 4 +--- lib/networkaccessmanager.h | 8 +++----- lib/ssosession.cpp | 2 +- lib/ssosession.h | 6 ++---- lib/user.cpp | 4 +--- lib/user.h | 5 ++--- lib/util.h | 41 +++++++++++++++++++++++++++++++++++++++++ 19 files changed, 75 insertions(+), 54 deletions(-) diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 77648562..9304a3de 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -47,15 +47,11 @@ public: mutable std::vector callbacks; }; -Avatar::Avatar() : d(std::make_unique()) {} +Avatar::Avatar() + : d(makeImpl()) +{} -Avatar::Avatar(QUrl url) : d(std::make_unique(std::move(url))) {} - -Avatar::Avatar(Avatar&&) = default; - -Avatar::~Avatar() = default; - -Avatar& Avatar::operator=(Avatar&&) = default; +Avatar::Avatar(QUrl url) : d(makeImpl(std::move(url))) {} QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const diff --git a/lib/avatar.h b/lib/avatar.h index 93f43948..c94dc369 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -3,13 +3,12 @@ #pragma once -#include "quotient_export.h" +#include "util.h" #include #include #include -#include namespace Quotient { class Connection; @@ -18,9 +17,6 @@ class QUOTIENT_API Avatar { public: explicit Avatar(); explicit Avatar(QUrl url); - Avatar(Avatar&&); - ~Avatar(); - Avatar& operator=(Avatar&&); using get_callback_t = std::function; using upload_callback_t = std::function; @@ -41,6 +37,6 @@ public: private: class Private; - std::unique_ptr d; + ImplPtr d; }; } // namespace Quotient diff --git a/lib/connection.cpp b/lib/connection.cpp index 8d1c80f1..1915c2a9 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -246,7 +246,8 @@ public: }; Connection::Connection(const QUrl& server, QObject* parent) - : QObject(parent), d(new Private(std::make_unique(server))) + : QObject(parent) + , d(makeImpl(std::make_unique(server))) { d->q = this; // All d initialization should occur before this line } diff --git a/lib/connection.h b/lib/connection.h index 23062664..b4476347 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -844,7 +844,7 @@ protected Q_SLOTS: private: class Private; - QScopedPointer d; + ImplPtr d; static room_factory_t _roomFactory; static user_factory_t _userFactory; diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index 87ad4577..aca218be 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -41,7 +41,7 @@ public: }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique(std::move(baseUrl))) + : d(makeImpl(std::move(baseUrl))) { // Each lambda invocation below takes no more than one job from the // queues (first foreground, then background) and resumes it; then diff --git a/lib/connectiondata.h b/lib/connectiondata.h index e16a2dac..75fc332f 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -4,9 +4,10 @@ #pragma once +#include "util.h" + #include -#include #include class QNetworkAccessManager; @@ -42,6 +43,6 @@ public: private: class Private; - std::unique_ptr d; + ImplPtr d; }; } // namespace Quotient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 971fea7b..f518a1b0 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -194,8 +194,8 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, const QUrlQuery& query, RequestData&& data, bool needsToken) - : d(new Private(verb, std::move(endpoint), query, std::move(data), - needsToken)) + : d(makeImpl(verb, std::move(endpoint), query, std::move(data), + needsToken)) { setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index f41fc63c..c899170d 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -467,7 +467,7 @@ private: void finishJob(); class Private; - QScopedPointer d; + ImplPtr d; }; inline bool QUOTIENT_API isJobPending(BaseJob* job) diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 6bf221cb..4a507ebd 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -32,13 +32,12 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename) : GetContentJob(serverName, mediaId) - , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) + , d(localFilename.isEmpty() ? makeImpl() + : makeImpl(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); } -DownloadFileJob::~DownloadFileJob() = default; - QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index 9e807fe7..f8c62e4b 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -13,13 +13,12 @@ public: DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename = {}); - ~DownloadFileJob() override; QString targetFileName() const; private: class Private; - QScopedPointer d; + ImplPtr d; void doPrepare() override; void onSentRequest(QNetworkReply* reply) override; diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index fb16c79f..d3cc3c37 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -17,7 +17,7 @@ public: }; MxcReply::MxcReply(QNetworkReply* reply) - : d(std::make_unique(reply)) + : d(makeImpl(reply)) { reply->setParent(this); connect(d->m_reply, &QNetworkReply::finished, this, [this]() { @@ -28,7 +28,7 @@ MxcReply::MxcReply(QNetworkReply* reply) } MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) - : d(std::make_unique(reply)) + : d(makeImpl(reply)) { reply->setParent(this); connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() { @@ -38,8 +38,6 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) }); } -MxcReply::~MxcReply() = default; - #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #define ERROR_SIGNAL errorOccurred #else @@ -47,6 +45,7 @@ MxcReply::~MxcReply() = default; #endif MxcReply::MxcReply() + : d(ZeroImpl()) { static const auto BadRequestPhrase = tr("Bad Request"); QMetaObject::invokeMethod(this, [this]() { diff --git a/lib/mxcreply.h b/lib/mxcreply.h index 1d31d608..f6c4a34d 100644 --- a/lib/mxcreply.h +++ b/lib/mxcreply.h @@ -3,10 +3,9 @@ #pragma once -#include "quotient_export.h" +#include "util.h" #include -#include namespace Quotient { class Room; @@ -18,7 +17,6 @@ public: explicit MxcReply(); explicit MxcReply(QNetworkReply *reply); MxcReply(QNetworkReply* reply, Room* room, const QString& eventId); - ~MxcReply() override; public Q_SLOTS: void abort() override; @@ -28,6 +26,6 @@ protected: private: class Private; - std::unique_ptr d; + ImplPtr d; }; } diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 57618329..2c0f716b 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -39,7 +39,7 @@ public: }; NetworkAccessManager::NetworkAccessManager(QObject* parent) - : QNetworkAccessManager(parent), d(std::make_unique(this)) + : QNetworkAccessManager(parent), d(makeImpl(this)) {} QList NetworkAccessManager::ignoredSslErrors() const @@ -79,8 +79,6 @@ NetworkAccessManager* NetworkAccessManager::instance() return storage.localData(); } -NetworkAccessManager::~NetworkAccessManager() = default; - QNetworkReply* NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index d06f9736..5a9c134c 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -3,20 +3,18 @@ #pragma once -#include "quotient_export.h" +#include "util.h" #include -#include - namespace Quotient { class Room; class Connection; + class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: NetworkAccessManager(QObject* parent = nullptr); - ~NetworkAccessManager() override; QList ignoredSslErrors() const; void addIgnoredSslError(const QSslError& error); @@ -33,6 +31,6 @@ private: QIODevice* outgoingData = Q_NULLPTR) override; class Private; - std::unique_ptr d; + ImplPtr d; }; } // namespace Quotient diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp index a1d27504..5f3479b8 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -61,7 +61,7 @@ public: SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId) : QObject(connection) - , d(std::make_unique(this, initialDeviceName, deviceId, connection)) + , d(makeImpl(this, initialDeviceName, deviceId, connection)) { qCDebug(MAIN) << "SSO session constructed"; } diff --git a/lib/ssosession.h b/lib/ssosession.h index a658c043..0f3fc3b8 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -3,13 +3,11 @@ #pragma once -#include "quotient_export.h" +#include "util.h" #include #include -#include - class QTcpServer; class QTcpSocket; @@ -44,6 +42,6 @@ public: private: class Private; - std::unique_ptr d; + ImplPtr d; }; } // namespace Quotient diff --git a/lib/user.cpp b/lib/user.cpp index 7da71dba..0dbc444a 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -46,7 +46,7 @@ public: decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId))) + : QObject(connection), d(makeImpl(move(userId))) { setObjectName(id()); if (connection->userId() == id()) { @@ -61,8 +61,6 @@ Connection* User::connection() const return static_cast(parent()); } -User::~User() = default; - void User::load() { auto* profileJob = diff --git a/lib/user.h b/lib/user.h index 435304ce..8412b7fd 100644 --- a/lib/user.h +++ b/lib/user.h @@ -5,7 +5,7 @@ #pragma once #include "avatar.h" -#include "quotient_export.h" +#include "util.h" #include @@ -27,7 +27,6 @@ class QUOTIENT_API User : public QObject { Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) public: User(QString userId, Connection* connection); - ~User() override; Connection* connection() const; @@ -126,7 +125,7 @@ Q_SIGNALS: private: class Private; - QScopedPointer d; + ImplPtr d; template bool doSetAvatar(SourceT&& source); diff --git a/lib/util.h b/lib/util.h index 399f93c2..f5650ee2 100644 --- a/lib/util.h +++ b/lib/util.h @@ -245,6 +245,47 @@ inline std::pair findFirstOf(InputIt first, InputIt last, return std::make_pair(last, sLast); } +//! \brief An owning implementation pointer +//! +//! This is basically std::unique_ptr<> to hold your pimpl's but without having +//! to define default constructors/operator=() out of line. +//! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html +//! for inspiration +template +using ImplPtr = std::unique_ptr; + +// Why this works (see also the link above): because this defers the moment +// of requiring sizeof of ImplType to the place where makeImpl is invoked +// (which is located, necessarily, in the .cpp file after ImplType definition). +// The stock unique_ptr deleter (std::default_delete) normally needs sizeof +// at the same spot - as long as you defer definition of the owning type +// constructors and operator='s to the .cpp file as well. Which means you +// have to explicitly declare and define them (even if with = default), +// formally breaking the rule of zero; informally, just adding boilerplate code. +// The custom deleter itself is instantiated at makeImpl invocation - there's +// no way earlier to even know how ImplType will be deleted and whether that +// will need sizeof(ImplType) earlier. In theory it's a tad slower because +// the deleter is called by the pointer; however, the difference will not +// be noticeable (if exist at all) for any class with non-trivial contents. + +//! \brief make_unique for ImplPtr +//! +//! Since std::make_unique is not compatible with ImplPtr, this should be used +//! in constructors of frontend classes to create implementation instances. +template +inline ImplPtr makeImpl(ArgTs&&... args) +{ + return ImplPtr { new ImplType(std::forward(args)...), + [](ImplType* impl) { delete impl; } }; +} + +template +const inline ImplPtr ZeroImpl() +{ + return { nullptr, [](ImplType*) {} }; +} + /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); -- cgit v1.2.3 From 9fc7bfd5ef1daf84307d553941855fb377fddc7c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 14:06:15 +0100 Subject: Add a comment, as Sonar advises --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index f5650ee2..66db0ece 100644 --- a/lib/util.h +++ b/lib/util.h @@ -283,7 +283,7 @@ inline ImplPtr makeImpl(ArgTs&&... args) template const inline ImplPtr ZeroImpl() { - return { nullptr, [](ImplType*) {} }; + return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; } /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ -- cgit v1.2.3 From c1bfb156844aa3c53b1588c48ba2002143199510 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 4 Jan 2022 20:47:49 +0100 Subject: .clang-format: slightly tweak penalties With the previous settings clang-format seemed too lenient to characters beyond position 80. [skip ci] --- .clang-format | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.clang-format b/.clang-format index 6e13223e..713d2165 100644 --- a/.clang-format +++ b/.clang-format @@ -104,8 +104,8 @@ PenaltyBreakComment: 45 #PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 200 #PenaltyBreakTemplateDeclaration: 10 -PenaltyExcessCharacter: 20 -PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyExcessCharacter: 40 +PenaltyReturnTypeOnItsOwnLine: 100 #PointerAlignment: Left #ReflowComments: true #SortIncludes: true -- cgit v1.2.3 From ecbff4c1a21ff4c0ab72141bc1a34ae189d33483 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 16:07:25 +0100 Subject: CMakeLists: add install(TARGETS) components again Older CMake versions fail if they don't find those (LGTM uses CMake 3.13 and that one does, at least). --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c889cf13..fd5f1dca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,6 +316,7 @@ endif() # Configure installation install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets + ARCHIVE LIBRARY RUNTIME INCLUDES DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} ) install(DIRECTORY lib/ DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} -- cgit v1.2.3 From 9e3752b8333813b9f00970a1af6e7ca9087ca424 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 14:37:07 +0100 Subject: Thumbnail: drop unneeded constructors Those are already inherited with 'using'. --- lib/events/eventcontent.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 87ea3672..de9a792b 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -149,10 +149,8 @@ namespace EventContent { */ class QUOTIENT_API Thumbnail : public ImageInfo { public: - Thumbnail() = default; // Allow empty thumbnails - Thumbnail(const QJsonObject& infoJson, const Omittable &file = none); - Thumbnail(const ImageInfo& info) : ImageInfo(info) {} using ImageInfo::ImageInfo; + Thumbnail(const QJsonObject& infoJson, const Omittable &file = none); /** * Writes thumbnail information to "thumbnail_info" subobject -- cgit v1.2.3 From 3986de7f8f1c98f952911c2f93891ea8643df62c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 15:08:57 +0100 Subject: Make TagRecord generally better It doesn't need all those things inside - order_type alias is no more in use; operator<() is better outside; QLatin1String is better to compare against than const char* (because const char* is assumed to be UTF-8); and TagRecord is really small so it doesn't need const& for parameters. --- lib/events/accountdataevents.h | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index c0f2202d..12f1f00b 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -4,27 +4,24 @@ #pragma once #include "event.h" +#include "util.h" namespace Quotient { -constexpr const char* FavouriteTag = "m.favourite"; -constexpr const char* LowPriorityTag = "m.lowpriority"; -constexpr const char* ServerNoticeTag = "m.server_notice"; +constexpr auto FavouriteTag [[maybe_unused]] = "m.favourite"_ls; +constexpr auto LowPriorityTag [[maybe_unused]] = "m.lowpriority"_ls; +constexpr auto ServerNoticeTag [[maybe_unused]] = "m.server_notice"_ls; struct TagRecord { - using order_type = Omittable; - - order_type order; - - TagRecord(order_type order = none) : order(order) {} - - bool operator<(const TagRecord& other) const - { - // Per The Spec, rooms with no order should be after those with order, - // against std::optional<>::operator<() convention. - return order && (!other.order || *order < *other.order); - } + Omittable order = none; }; +inline bool operator<(TagRecord lhs, TagRecord rhs) +{ + // Per The Spec, rooms with no order should be after those with order, + // against std::optional<>::operator<() convention. + return lhs.order && (!rhs.order || *lhs.order < *rhs.order); +} + template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, TagRecord& rec) @@ -41,7 +38,7 @@ struct JsonObjectConverter { rec.order = none; } } - static void dumpTo(QJsonObject& jo, const TagRecord& rec) + static void dumpTo(QJsonObject& jo, TagRecord rec) { addParam(jo, QStringLiteral("order"), rec.order); } -- cgit v1.2.3 From 9351d9afcbaae0bdc8aa26f7361be1f84cac7467 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 15:13:24 +0100 Subject: DEFINE_EVENT_TYPEID: fix up deprecation warnings --- lib/events/event.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index f12e525e..858972da 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -264,9 +264,9 @@ using Events = EventsArray; // provide matrixTypeId() and typeId(). #define DEFINE_EVENT_TYPEID(Id_, Type_) \ static constexpr event_type_t TypeId = Id_##_ls; \ - [[deprecated("Use _Type::TypeId directly instead")]] \ + [[deprecated("Use " #Type_ "::TypeId directly instead")]] \ static constexpr event_mtype_t matrixTypeId() { return Id_; } \ - [[deprecated("Use _Type::TypeId directly instead")]] \ + [[deprecated("Use " #Type_ "::TypeId directly instead")]] \ static event_type_t typeId() { return TypeId; } \ // End of macro -- cgit v1.2.3 From c483ef95bd3e5effd6dd08c6d0ea0f83d0aec051 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 15:14:33 +0100 Subject: Fully-qualify types passed to slots --- lib/jobs/basejob.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index c899170d..1567e635 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -248,7 +248,7 @@ public: } public Q_SLOTS: - void initiate(ConnectionData* connData, bool inBackground); + void initiate(Quotient::ConnectionData* connData, bool inBackground); /** * Abandons the result of this job, arrived or unarrived. -- 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(-) 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 bc4a0f5d408d901f3c8f4dfeec0574ded04845bf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 20:13:44 +0100 Subject: Brush up SsoSession; document Connection::prepareForSso Although parented to Connection, SsoSession was pretty leaky in that unsuccessful login attempts didn't delete the object and in some errors didn't even close the local HTTP socket (though new connections would no more be accepted). Also, without the documentation it wasn't clear who owns the object returned by Connection::prepareForSso(). Now it is. Unfortunately, it's not easy to cover SsoSession with tests. Basically, it takes a homeserver and a mock "SSO agent" that would check the SSO URL for validity and then both send the login authorisation to the homeserver as well as ping the callback given by SsoSession. Maybe for another time. --- lib/connection.h | 11 +++++++++++ lib/ssosession.cpp | 42 +++++++++++++++++++++--------------------- lib/ssosession.h | 6 ++---- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index b4476347..28688cc1 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -443,6 +443,17 @@ public: std::forward(jobArgs)...); } + //! \brief Start a local HTTP server and generate a single sign-on URL + //! + //! This call does the preparatory steps to carry out single sign-on + //! sequence + //! \sa https://matrix.org/docs/guides/sso-for-client-developers + //! \return A proxy object holding two URLs: one for SSO on the chosen + //! homeserver and another for the local callback address. Normally + //! you won't need the callback URL unless you proxy the response + //! with a custom UI. You do not need to delete the SsoSession + //! object; the Connection that issued it will dispose of it once + //! the login sequence completes (with any outcome). Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName, const QString& deviceId = {}); diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp index 5f3479b8..93e252cc 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -15,10 +15,10 @@ using namespace Quotient; class SsoSession::Private { public: - Private(SsoSession* q, const QString& initialDeviceName = {}, - const QString& deviceId = {}, Connection* connection = nullptr) - : initialDeviceName(initialDeviceName) - , deviceId(deviceId) + Private(SsoSession* q, QString initialDeviceName = {}, + QString deviceId = {}, Connection* connection = nullptr) + : initialDeviceName(std::move(initialDeviceName)) + , deviceId(std::move(deviceId)) , connection(connection) { auto* server = new QTcpServer(q); @@ -29,7 +29,7 @@ public: .arg(server->serverPort()); ssoUrl = connection->getUrlForApi(callbackUrl); - QObject::connect(server, &QTcpServer::newConnection, q, [this, server] { + QObject::connect(server, &QTcpServer::newConnection, q, [this, q, server] { qCDebug(MAIN) << "SSO callback initiated"; socket = server->nextPendingConnection(); server->close(); @@ -43,8 +43,14 @@ public: }); QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater); + QObject::connect(socket, &QObject::destroyed, q, + &QObject::deleteLater); }); + qCDebug(MAIN) << "SSO session constructed"; } + ~Private() { qCDebug(MAIN) << "SSO session deconstructed"; } + Q_DISABLE_COPY_MOVE(Private) + void processCallback(); void sendHttpResponse(const QByteArray& code, const QByteArray& msg); void onError(const QByteArray& code, const QString& errorMsg); @@ -62,14 +68,7 @@ SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId) : QObject(connection) , d(makeImpl(this, initialDeviceName, deviceId, connection)) -{ - qCDebug(MAIN) << "SSO session constructed"; -} - -SsoSession::~SsoSession() -{ - qCDebug(MAIN) << "SSO session deconstructed"; -} +{} QUrl SsoSession::ssoUrl() const { return d->ssoUrl; } @@ -82,29 +81,29 @@ void SsoSession::Private::processCallback() // (see at https://github.com/clementine-player/Clementine/) const auto& requestParts = requestData.split(' '); if (requestParts.size() < 2 || requestParts[1].isEmpty()) { - onError("400 Bad Request", tr("No login token in SSO callback")); + onError("400 Bad Request", tr("Malformed single sign-on callback")); return; } const auto& QueryItemName = QStringLiteral("loginToken"); QUrlQuery query { QUrl(requestParts[1]).query() }; if (!query.hasQueryItem(QueryItemName)) { - onError("400 Bad Request", tr("Malformed single sign-on callback")); + onError("400 Bad Request", tr("No login token in SSO callback")); + return; } qCDebug(MAIN) << "Found the token in SSO callback, logging in"; connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(), initialDeviceName, deviceId); connect(connection, &Connection::connected, socket, [this] { - const QString msg = - "The application '" % QCoreApplication::applicationName() - % "' has successfully logged in as a user " % connection->userId() - % " with device id " % connection->deviceId() - % ". This window can be closed. Thank you.\r\n"; + const auto msg = + tr("The application '%1' has successfully logged in as a user %2 " + "with device id %3. This window can be closed. Thank you.\r\n") + .arg(QCoreApplication::applicationName(), connection->userId(), + connection->deviceId()); sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8()); socket->disconnectFromHost(); }); connect(connection, &Connection::loginError, socket, [this] { onError("401 Unauthorised", tr("Login failed")); - socket->disconnectFromHost(); }); } @@ -128,4 +127,5 @@ void SsoSession::Private::onError(const QByteArray& code, // [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have // an intermediate signal but that seems just a fight for purity. emit connection->loginError(errorMsg, requestData); + socket->disconnectFromHost(); } diff --git a/lib/ssosession.h b/lib/ssosession.h index 0f3fc3b8..e6a3f8fb 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -8,9 +8,6 @@ #include #include -class QTcpServer; -class QTcpSocket; - namespace Quotient { class Connection; @@ -36,7 +33,8 @@ class QUOTIENT_API SsoSession : public QObject { public: SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId = {}); - ~SsoSession() override; + ~SsoSession() override = default; + QUrl ssoUrl() const; QUrl callbackUrl() const; -- cgit v1.2.3 From bd280a087ecab30f94f7937513ee298c233fcba1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 18 Jan 2022 08:54:52 +0100 Subject: Revise inline keyword usage - Templates and constexpr imply inline - A function called from a single site better be inlined. --- lib/events/event.h | 2 +- lib/jobs/basejob.h | 2 +- lib/quotient_common.h | 8 +++----- lib/uri.cpp | 2 +- lib/util.h | 8 ++++---- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 858972da..f10f6a8d 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -311,7 +311,7 @@ inline auto switchOnType(const BaseEventT& event, FnT&& fn) namespace _impl { // Using bool instead of auto below because auto apparently upsets MSVC template - inline constexpr bool needs_downcast = + constexpr bool needs_downcast = std::is_base_of_v>> && !std::is_same_v>>; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 1567e635..9ed58ba8 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -28,7 +28,7 @@ class QUOTIENT_API BaseJob : public QObject { static QByteArray encodeIfParam(const QString& paramPart); template - static inline auto encodeIfParam(const char (&constPart)[N]) + static auto encodeIfParam(const char (&constPart)[N]) { return constPart; } diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 3d38ce1f..af3e2730 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -77,7 +77,7 @@ enum class Membership : unsigned int { }; QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) -constexpr inline auto MembershipStrings = make_array( +constexpr auto MembershipStrings = make_array( // The order MUST be the same as the order in the original enum "join", "leave", "invite", "knock", "ban"); @@ -95,7 +95,7 @@ enum class JoinState : std::underlying_type_t { }; QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) -constexpr inline auto JoinStateStrings = make_array( +constexpr auto JoinStateStrings = make_array( MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ ); @@ -125,9 +125,7 @@ enum RoomType { }; Q_ENUM_NS(RoomType) -constexpr inline auto RoomTypeStrings = make_array( - "m.space" -); +constexpr auto RoomTypeStrings = make_array("m.space"); } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) diff --git a/lib/uri.cpp b/lib/uri.cpp index c8843dda..3ce81a21 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -75,7 +75,7 @@ static auto decodeFragmentPart(QStringView part) return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); } -static auto matrixToUrlRegexInit() +static inline auto matrixToUrlRegexInit() { // See https://matrix.org/docs/spec/appendices#matrix-to-navigation const QRegularExpression MatrixToUrlRE { diff --git a/lib/util.h b/lib/util.h index 66db0ece..3505b62f 100644 --- a/lib/util.h +++ b/lib/util.h @@ -54,9 +54,9 @@ using UnorderedMap = std::unordered_map>; namespace _impl { template - constexpr inline auto IsOmittableValue = false; + constexpr auto IsOmittableValue = false; template - constexpr inline auto IsOmittable = IsOmittableValue>; + constexpr auto IsOmittable = IsOmittableValue>; } constexpr auto none = std::nullopt; @@ -165,7 +165,7 @@ Omittable(T&&) -> Omittable; namespace _impl { template - constexpr inline auto IsOmittableValue> = true; + constexpr auto IsOmittableValue> = true; } template @@ -191,7 +191,7 @@ inline auto merge(T1& lhs, const Omittable& rhs) return true; } -inline constexpr auto operator"" _ls(const char* s, std::size_t size) +constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); } -- cgit v1.2.3 From aa2465bb7991876c3b5afb5afda54f43fefd9e0a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 6 Jan 2022 12:04:42 +0100 Subject: .clang-format: try harder to avoid return type on its own line In reality it's often not just the return type but the return type and the preceding attribute, in particular [[deprecated("...")]] - and unfortunately this attribute (and any other, actually) cannot be given its own line using ClangFormat. So at least try to glue those two to the function name as much as possible... --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 713d2165..8375204a 100644 --- a/.clang-format +++ b/.clang-format @@ -105,7 +105,7 @@ PenaltyBreakComment: 45 PenaltyBreakString: 200 #PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 40 -PenaltyReturnTypeOnItsOwnLine: 100 +PenaltyReturnTypeOnItsOwnLine: 150 #PointerAlignment: Left #ReflowComments: true #SortIncludes: true -- cgit v1.2.3 From aaa57e0e458435e23047fcb325872f0350171bad Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 14 Jan 2022 22:37:28 +0100 Subject: AccountRegistry: derive from QVector and clean up Notably, Quotient::AccountRegistry::instance() is now deprecated in favour of Quotient::Accounts inline variable. --- lib/accountregistry.cpp | 73 +++++++++++++------------------------------- lib/accountregistry.h | 49 ++++++++++++++++++----------- lib/connection.cpp | 4 +-- lib/networkaccessmanager.cpp | 2 +- 4 files changed, 55 insertions(+), 73 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index a292ed45..616b54b4 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -8,90 +8,59 @@ using namespace Quotient; -void AccountRegistry::add(Connection* c) +void AccountRegistry::add(Connection* a) { - if (m_accounts.contains(c)) + if (contains(a)) return; - beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size()); - m_accounts += c; + beginInsertRows(QModelIndex(), size(), size()); + push_back(a); endInsertRows(); } -void AccountRegistry::drop(Connection* c) +void AccountRegistry::drop(Connection* a) { - beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c)); - m_accounts.removeOne(c); + const auto idx = indexOf(a); + beginRemoveRows(QModelIndex(), idx, idx); + remove(idx); endRemoveRows(); - Q_ASSERT(!m_accounts.contains(c)); + Q_ASSERT(!contains(a)); } bool AccountRegistry::isLoggedIn(const QString &userId) const { - return std::any_of(m_accounts.cbegin(), m_accounts.cend(), - [&userId](Connection* a) { return a->userId() == userId; }); + return std::any_of(cbegin(), cend(), [&userId](const Connection* a) { + return a->userId() == userId; + }); } -bool AccountRegistry::contains(Connection *c) const +QVariant AccountRegistry::data(const QModelIndex& index, int role) const { - return m_accounts.contains(c); -} - -AccountRegistry::AccountRegistry() = default; - -QVariant AccountRegistry::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - return {}; - } - - if (index.row() >= m_accounts.count()) { + if (!index.isValid() || index.row() >= count()) return {}; - } - auto account = m_accounts[index.row()]; - - if (role == ConnectionRole) { - return QVariant::fromValue(account); - } + if (role == AccountRole) + return QVariant::fromValue(at(index.row())); return {}; } -int AccountRegistry::rowCount(const QModelIndex &parent) const +int AccountRegistry::rowCount(const QModelIndex& parent) const { - if (parent.isValid()) { - return 0; - } - - return m_accounts.count(); + return parent.isValid() ? 0 : count(); } QHash AccountRegistry::roleNames() const { - return {{ConnectionRole, "connection"}}; + return { { AccountRole, "connection" } }; } -bool AccountRegistry::isEmpty() const -{ - return m_accounts.isEmpty(); -} -int AccountRegistry::count() const -{ - return m_accounts.count(); -} - -const QVector AccountRegistry::accounts() const -{ - return m_accounts; -} Connection* AccountRegistry::get(const QString& userId) { - for (const auto &connection : m_accounts) { - if(connection->userId() == userId) { + for (const auto &connection : *this) { + if (connection->userId() == userId) return connection; - } } return nullptr; } diff --git a/lib/accountregistry.h b/lib/accountregistry.h index f7a864df..2f6dffdf 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -6,42 +6,55 @@ #include "quotient_export.h" -#include -#include #include namespace Quotient { class Connection; -class QUOTIENT_API AccountRegistry : public QAbstractListModel { +class QUOTIENT_API AccountRegistry : public QAbstractListModel, + private QVector { Q_OBJECT public: + using const_iterator = QVector::const_iterator; + using const_reference = QVector::const_reference; + enum EventRoles { - ConnectionRole = Qt::UserRole + 1, + AccountRole = Qt::UserRole + 1, + ConnectionRole = AccountRole }; - static AccountRegistry &instance() { - static AccountRegistry _instance; - return _instance; - } + [[deprecated("Use Accounts variable instead")]] // + static AccountRegistry& instance(); + + // Expose most of QVector's const-API but only provide add() and drop() + // for changing it. In theory other changing operations could be supported + // too; but then boilerplate begin/end*() calls has to be tucked into each + // and this class gives no guarantees on the order of entries, so why care. - const QVector accounts() const; + const QVector& accounts() const { return *this; } void add(Connection* a); void drop(Connection* a); + const_iterator begin() const { return QVector::begin(); } + const_iterator end() const { return QVector::end(); } + const_reference front() const { return QVector::front(); } + const_reference back() const { return QVector::back(); } bool isLoggedIn(const QString& userId) const; - bool isEmpty() const; - int count() const; - bool contains(Connection*) const; Connection* get(const QString& userId); - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + using QVector::isEmpty, QVector::empty; + using QVector::size, QVector::count, QVector::capacity; + using QVector::cbegin, QVector::cend, QVector::contains; + + // QAbstractItemModel interface implementation + [[nodiscard]] QVariant data(const QModelIndex& index, + int role) const override; + [[nodiscard]] int rowCount( + const QModelIndex& parent = QModelIndex()) const override; [[nodiscard]] QHash roleNames() const override; +}; -private: - AccountRegistry(); +inline QUOTIENT_API AccountRegistry Accounts {}; - QVector m_accounts; -}; +inline AccountRegistry& AccountRegistry::instance() { return Accounts; } } diff --git a/lib/connection.cpp b/lib/connection.cpp index 1915c2a9..67bc83f9 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -258,7 +258,7 @@ Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); - AccountRegistry::instance().drop(this); + Accounts.drop(this); } void Connection::resolveServer(const QString& mxid) @@ -438,7 +438,7 @@ void Connection::Private::completeSetup(const QString& mxId) qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << data->userId() << "from device" << data->deviceId(); - AccountRegistry::instance().add(q); + Accounts.add(q); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 2c0f716b..58c3cc3a 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -97,7 +97,7 @@ QNetworkReply* NetworkAccessManager::createRequest( // TODO: Make the best effort with a direct unauthenticated request // to the media server } else { - auto* const connection = AccountRegistry::instance().get(accountId); + auto* const connection = Accounts.get(accountId); if (!connection) { qCWarning(NETWORK) << "Connection" << accountId << "not found"; return new MxcReply(); -- cgit v1.2.3 From b17b629df44dbc888f6b6bee79d0d13d662371cf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 18 Jan 2022 08:55:10 +0100 Subject: Add [[maybe_unused]] to things the lib doesn't use --- lib/function_traits.cpp | 2 +- lib/quotient_common.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp index 20bcf30e..6542101a 100644 --- a/lib/function_traits.cpp +++ b/lib/function_traits.cpp @@ -47,7 +47,7 @@ static_assert(std::is_same_v, int>, "Test fn_arg_t defaulting to first argument"); template -static void ft(const std::vector&); +[[maybe_unused]] static void ft(const std::vector&); static_assert( std::is_same)>, const std::vector&>(), "Test function templates"); diff --git a/lib/quotient_common.h b/lib/quotient_common.h index af3e2730..02a9f0cd 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -95,7 +95,7 @@ enum class JoinState : std::underlying_type_t { }; QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) -constexpr auto JoinStateStrings = make_array( +[[maybe_unused]] constexpr auto JoinStateStrings = make_array( MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ ); @@ -125,7 +125,7 @@ enum RoomType { }; Q_ENUM_NS(RoomType) -constexpr auto RoomTypeStrings = make_array("m.space"); +[[maybe_unused]] constexpr auto RoomTypeStrings = make_array("m.space"); } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) -- cgit v1.2.3 From 1010c8d0009293a6ae357c23e8b4732302b6b8bd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 18 Jan 2022 06:42:42 +0100 Subject: Drop unused forward declarations --- lib/networkaccessmanager.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 5a9c134c..8ff1c6b5 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -8,8 +8,6 @@ #include namespace Quotient { -class Room; -class Connection; class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT -- cgit v1.2.3 From c7907084282c7957d085acb329574ab6a7d593c8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 18 Jan 2022 11:43:32 +0100 Subject: Move over non-interface code to QLatin1String It's better than const char* because any interaction between const char* and QString assumes that const char* contains UTF-8, which is pessimistic and therefore inefficient; at the same time: - construction of QString from QLatin1String is extremely fast (boiling down to padding null bytes) - "something"_ls is much shorter than QStringLiteral("something") - "something"_ls produces a direct pointer to the literal at compile time, using the benefits of raw string literals (deduplication, e.g.) The library API will also transition to QLatin1String where applicable, likely in 0.8. --- lib/events/roommessageevent.cpp | 64 ++++++++++++++++++++--------------------- lib/settings.cpp | 9 +++--- lib/uri.cpp | 19 ++++++------ 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 2b7b4166..0f58d8a6 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -19,15 +19,13 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; -static const auto RelatesToKeyL = "m.relates_to"_ls; -static const auto MsgTypeKeyL = "msgtype"_ls; -static const auto FormattedBodyKeyL = "formatted_body"_ls; - -static const auto TextTypeKey = "m.text"; -static const auto EmoteTypeKey = "m.emote"; -static const auto NoticeTypeKey = "m.notice"; - -static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); +static constexpr auto RelatesToKey = "m.relates_to"_ls; +static constexpr auto MsgTypeKey = "msgtype"_ls; +static constexpr auto FormattedBodyKey = "formatted_body"_ls; +static constexpr auto TextTypeKey = "m.text"_ls; +static constexpr auto EmoteTypeKey = "m.emote"_ls; +static constexpr auto NoticeTypeKey = "m.notice"_ls; +static constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls; template TypedBase* make(const QJsonObject& json) @@ -38,13 +36,13 @@ TypedBase* make(const QJsonObject& json) template <> TypedBase* make(const QJsonObject& json) { - return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) ? new TextContent(json) : nullptr; } struct MsgTypeDesc { - QString matrixType; + QLatin1String matrixType; MsgType enumType; TypedBase* (*maker)(const QJsonObject&); }; @@ -53,11 +51,11 @@ const std::vector msgTypes = { { TextTypeKey, MsgType::Text, make }, { EmoteTypeKey, MsgType::Emote, make }, { NoticeTypeKey, MsgType::Notice, make }, - { QStringLiteral("m.image"), MsgType::Image, make }, - { QStringLiteral("m.file"), MsgType::File, make }, - { QStringLiteral("m.location"), MsgType::Location, make }, - { QStringLiteral("m.video"), MsgType::Video, make }, - { QStringLiteral("m.audio"), MsgType::Audio, make } + { "m.image"_ls, MsgType::Image, make }, + { "m.file"_ls, MsgType::File, make }, + { "m.location"_ls, MsgType::Location, make }, + { "m.video"_ls, MsgType::Video, make }, + { "m.audio"_ls, MsgType::Audio, make } }; QString msgTypeToJson(MsgType enumType) @@ -94,12 +92,12 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); - if (json.contains(RelatesToKeyL)) { + if (json.contains(RelatesToKey)) { if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && jsonMsgType != EmoteTypeKey) { - json.remove(RelatesToKeyL); + json.remove(RelatesToKey); qCWarning(EVENTS) - << RelatesToKeyL << "cannot be used in" << jsonMsgType + << RelatesToKey << "cannot be used in" << jsonMsgType << "messages; the relation has been stripped off"; } else { // After the above, we know for sure that the content is TextContent @@ -109,9 +107,9 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { auto newContentJson = json.take("m.new_content"_ls).toObject(); newContentJson.insert(BodyKey, plainBody); - newContentJson.insert(MsgTypeKeyL, jsonMsgType); + newContentJson.insert(MsgTypeKey, jsonMsgType); json.insert(QStringLiteral("m.new_content"), newContentJson); - json[MsgTypeKeyL] = jsonMsgType; + json[MsgTypeKey] = jsonMsgType; json[BodyKeyL] = "* " + plainBody; return json; } @@ -177,8 +175,8 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if (isRedacted()) return; const QJsonObject content = contentJson(); - if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) { - auto msgtype = content[MsgTypeKeyL].toString(); + if (content.contains(MsgTypeKey) && content.contains(BodyKeyL)) { + auto msgtype = content[MsgTypeKey].toString(); bool msgTypeFound = false; for (const auto& mt : msgTypes) if (mt.matrixType == msgtype) { @@ -204,7 +202,7 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentPart(MsgTypeKeyL); + return contentPart(MsgTypeKey); } QString RoomMessageEvent::plainBody() const @@ -295,7 +293,7 @@ Omittable fromJson(const QJsonValue& jv) } // namespace Quotient TextContent::TextContent(const QJsonObject& json) - : relatesTo(fromJson>(json[RelatesToKeyL])) + : relatesTo(fromJson>(json[RelatesToKey])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); @@ -308,7 +306,7 @@ TextContent::TextContent(const QJsonObject& json) // of sending HTML messages. if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = actualJson[FormattedBodyKeyL].toString(); + body = actualJson[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. @@ -320,7 +318,6 @@ TextContent::TextContent(const QJsonObject& json) void TextContent::fillJson(QJsonObject* json) const { static const auto FormatKey = QStringLiteral("format"); - static const auto FormattedBodyKey = QStringLiteral("formatted_body"); Q_ASSERT(json); if (mimeType.inherits("text/html")) { @@ -328,11 +325,14 @@ void TextContent::fillJson(QJsonObject* json) const json->insert(FormattedBodyKey, body); } if (relatesTo) { - json->insert(QStringLiteral("m.relates_to"), - relatesTo->type == RelatesTo::ReplyTypeId() ? - QJsonObject { { relatesTo->type, QJsonObject{ { EventIdKey, relatesTo->eventId } } } } : - QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } } - ); + json->insert( + QStringLiteral("m.relates_to"), + relatesTo->type == RelatesTo::ReplyTypeId() + ? QJsonObject { { relatesTo->type, + QJsonObject { + { EventIdKey, relatesTo->eventId } } } } + : QJsonObject { { "rel_type", relatesTo->type }, + { EventIdKey, relatesTo->eventId } }); if (relatesTo->type == RelatesTo::ReplacementTypeId()) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { diff --git a/lib/settings.cpp b/lib/settings.cpp index 5549e4de..20734a7e 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -3,6 +3,7 @@ #include "settings.h" +#include "util.h" #include "logging.h" #include @@ -109,10 +110,10 @@ QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) -static const auto HomeserverKey = QStringLiteral("homeserver"); -static const auto AccessTokenKey = QStringLiteral("access_token"); -static const auto EncryptionAccountPickleKey = - QStringLiteral("encryption_account_pickle"); +static constexpr auto HomeserverKey = "homeserver"_ls; +static constexpr auto AccessTokenKey = "access_token"_ls; +static constexpr auto EncryptionAccountPickleKey = + "encryption_account_pickle"_ls; QUrl AccountSettings::homeserver() const { diff --git a/lib/uri.cpp b/lib/uri.cpp index 3ce81a21..11c59b69 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -3,27 +3,28 @@ #include "uri.h" +#include "util.h" #include "logging.h" #include using namespace Quotient; -struct ReplacePair { QByteArray uriString; char sigil; }; +struct ReplacePair { QLatin1String uriString; char sigil; }; /// \brief Defines bi-directional mapping of path prefixes and sigils /// /// When there are two prefixes for the same sigil, the first matching /// entry for a given sigil is used. -static const auto replacePairs = { - ReplacePair { "u/", '@' }, - { "user/", '@' }, - { "roomid/", '!' }, - { "r/", '#' }, - { "room/", '#' }, +static const ReplacePair replacePairs[] = { + { "u/"_ls, '@' }, + { "user/"_ls, '@' }, + { "roomid/"_ls, '!' }, + { "r/"_ls, '#' }, + { "room/"_ls, '#' }, // The notation for bare event ids is not proposed in MSC2312 but there's // https://github.com/matrix-org/matrix-doc/pull/2644 - { "e/", '$' }, - { "event/", '$' } + { "e/"_ls, '$' }, + { "event/"_ls, '$' } }; Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) -- cgit v1.2.3 From bf82aeea369cacfc93a0e6d6d9feb01f1f2afdb2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 18 Jan 2022 11:43:21 +0100 Subject: Don't use 'static' on top-level/namespace scope When internal linkage is necessary, anonymous namespaces fulfil the same purpose in a better way. See also: https://stackoverflow.com/questions/4422507/superiority-of-unnamed-namespace-over-static --- lib/converters.h | 2 +- lib/events/roommessageevent.cpp | 18 +++++++++++------- lib/settings.cpp | 9 +++++---- lib/uri.cpp | 6 +++++- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index a6028f1b..8eea1cd3 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -355,7 +355,7 @@ namespace _impl { }; } // namespace _impl -static constexpr bool IfNotEmpty = false; +constexpr bool IfNotEmpty = false; /*! Add a key-value pair to QJsonObject or QUrlQuery * diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 0f58d8a6..5ab0f845 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -19,13 +19,15 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; -static constexpr auto RelatesToKey = "m.relates_to"_ls; -static constexpr auto MsgTypeKey = "msgtype"_ls; -static constexpr auto FormattedBodyKey = "formatted_body"_ls; -static constexpr auto TextTypeKey = "m.text"_ls; -static constexpr auto EmoteTypeKey = "m.emote"_ls; -static constexpr auto NoticeTypeKey = "m.notice"_ls; -static constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls; +namespace { // Supporting internal definitions + +constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto MsgTypeKey = "msgtype"_ls; +constexpr auto FormattedBodyKey = "formatted_body"_ls; +constexpr auto TextTypeKey = "m.text"_ls; +constexpr auto EmoteTypeKey = "m.emote"_ls; +constexpr auto NoticeTypeKey = "m.notice"_ls; +constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls; template TypedBase* make(const QJsonObject& json) @@ -87,6 +89,8 @@ inline bool isReplacement(const Omittable& rel) return rel && rel->type == RelatesTo::ReplacementTypeId(); } +} // anonymous namespace + QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) diff --git a/lib/settings.cpp b/lib/settings.cpp index 20734a7e..2491d89d 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -110,10 +110,11 @@ QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) -static constexpr auto HomeserverKey = "homeserver"_ls; -static constexpr auto AccessTokenKey = "access_token"_ls; -static constexpr auto EncryptionAccountPickleKey = - "encryption_account_pickle"_ls; +namespace { +constexpr auto HomeserverKey = "homeserver"_ls; +constexpr auto AccessTokenKey = "access_token"_ls; +constexpr auto EncryptionAccountPickleKey = "encryption_account_pickle"_ls; +} QUrl AccountSettings::homeserver() const { diff --git a/lib/uri.cpp b/lib/uri.cpp index 11c59b69..6b7d1d20 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -10,12 +10,14 @@ using namespace Quotient; +namespace { + struct ReplacePair { QLatin1String uriString; char sigil; }; /// \brief Defines bi-directional mapping of path prefixes and sigils /// /// When there are two prefixes for the same sigil, the first matching /// entry for a given sigil is used. -static const ReplacePair replacePairs[] = { +const ReplacePair replacePairs[] = { { "u/"_ls, '@' }, { "user/"_ls, '@' }, { "roomid/"_ls, '!' }, @@ -27,6 +29,8 @@ static const ReplacePair replacePairs[] = { { "event/"_ls, '$' } }; +} + Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) { if (primaryId.isEmpty()) -- cgit v1.2.3 From e1ffc58ab9abf6321f92a6b648d6f8da08b0924d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 19 Jan 2022 13:33:32 +0100 Subject: CMakeLists: Bring back ARCHIVE DESTINATION in install() The older CMake used by LGTM is still unhappy without it. (See also recent changes to this file.) --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd5f1dca..c4e97673 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,7 +316,8 @@ endif() # Configure installation install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets - ARCHIVE LIBRARY RUNTIME + LIBRARY RUNTIME + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} ) install(DIRECTORY lib/ DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} -- 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). --- CMakeLists.txt | 3 ++- lib/events/eventrelation.cpp | 38 ++++++++++++++++++++++++++++++ lib/events/eventrelation.h | 52 +++++++++++++++++++++++++++++++++++++++++ lib/events/reactionevent.cpp | 29 ----------------------- lib/events/reactionevent.h | 32 ++----------------------- lib/events/roommessageevent.cpp | 38 ++++++++---------------------- lib/events/roommessageevent.h | 27 +++++++++++---------- lib/room.cpp | 11 ++++----- lib/room.h | 5 ++-- quotest/quotest.cpp | 2 +- 10 files changed, 128 insertions(+), 109 deletions(-) create mode 100644 lib/events/eventrelation.cpp create mode 100644 lib/events/eventrelation.h delete mode 100644 lib/events/reactionevent.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fd5f1dca..df193b94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,6 +153,7 @@ list(APPEND lib_SRCS lib/events/stateevent.h lib/events/stateevent.cpp lib/events/simplestateevents.h lib/events/eventcontent.h lib/events/eventcontent.cpp + lib/events/eventrelation.h lib/events/eventrelation.cpp lib/events/roomcreateevent.h lib/events/roomcreateevent.cpp lib/events/roomtombstoneevent.h lib/events/roomtombstoneevent.cpp lib/events/roommessageevent.h lib/events/roommessageevent.cpp @@ -162,7 +163,7 @@ list(APPEND lib_SRCS lib/events/typingevent.h lib/events/typingevent.cpp lib/events/accountdataevents.h lib/events/receiptevent.h lib/events/receiptevent.cpp - lib/events/reactionevent.h lib/events/reactionevent.cpp + lib/events/reactionevent.h lib/events/callinviteevent.h lib/events/callinviteevent.cpp lib/events/callcandidatesevent.h lib/events/callanswerevent.h lib/events/callanswerevent.cpp diff --git a/lib/events/eventrelation.cpp b/lib/events/eventrelation.cpp new file mode 100644 index 00000000..04972f45 --- /dev/null +++ b/lib/events/eventrelation.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "eventrelation.h" + +#include "../logging.h" +#include "event.h" + +using namespace Quotient; + +void JsonObjectConverter::dumpTo(QJsonObject& jo, + const EventRelation& pod) +{ + if (pod.type.isEmpty()) { + qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; + return; + } + jo.insert(RelTypeKey, pod.type); + jo.insert(EventIdKey, pod.eventId); + if (pod.type == EventRelation::AnnotationType) + jo.insert(QStringLiteral("key"), pod.key); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, + EventRelation& pod) +{ + if (const auto replyJson = jo.value(EventRelation::ReplyType).toObject(); + !replyJson.isEmpty()) { + pod.type = EventRelation::ReplyType; + fromJson(replyJson[EventIdKeyL], pod.eventId); + } else { + // The experimental logic for generic relationships (MSC1849) + fromJson(jo[RelTypeKey], pod.type); + fromJson(jo[EventIdKeyL], pod.eventId); + if (pod.type == EventRelation::AnnotationType) + fromJson(jo["key"_ls], pod.key); + } +} diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h new file mode 100644 index 00000000..e445ee42 --- /dev/null +++ b/lib/events/eventrelation.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +namespace Quotient { + +[[maybe_unused]] constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto RelTypeKey = "rel_type"_ls; + +struct QUOTIENT_API EventRelation { + using reltypeid_t = QLatin1String; + + QString type; + QString eventId; + QString key = {}; // Only used for m.annotation for now + + static constexpr auto ReplyType = "m.in_reply_to"_ls; + static constexpr auto AnnotationType = "m.annotation"_ls; + static constexpr auto ReplacementType = "m.replace"_ls; + + static EventRelation replyTo(QString eventId) + { + return { ReplyType, std::move(eventId) }; + } + static EventRelation annotate(QString eventId, QString key) + { + return { AnnotationType, std::move(eventId), std::move(key) }; + } + static EventRelation replace(QString eventId) + { + return { ReplacementType, std::move(eventId) }; + } + + [[deprecated("Use ReplyRelation variable instead")]] + static constexpr auto Reply() { return ReplyType; } + [[deprecated("Use AnnotationRelation variable instead")]] // + static constexpr auto Annotation() { return AnnotationType; } + [[deprecated("Use ReplacementRelation variable instead")]] // + static constexpr auto Replacement() { return ReplacementType; } +}; + +template <> +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const EventRelation& pod); + static void fillFrom(const QJsonObject& jo, EventRelation& pod); +}; + +} + diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp deleted file mode 100644 index b53fffd6..00000000 --- a/lib/events/reactionevent.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "reactionevent.h" - -using namespace Quotient; - -void JsonObjectConverter::dumpTo( - QJsonObject& jo, const EventRelation& pod) -{ - if (pod.type.isEmpty()) { - qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; - return; - } - jo.insert(QStringLiteral("rel_type"), pod.type); - jo.insert(EventIdKey, pod.eventId); - if (pod.type == EventRelation::Annotation()) - jo.insert(QStringLiteral("key"), pod.key); -} - -void JsonObjectConverter::fillFrom( - const QJsonObject& jo, EventRelation& pod) -{ - // The experimental logic for generic relationships (MSC1849) - fromJson(jo["rel_type"_ls], pod.type); - fromJson(jo[EventIdKeyL], pod.eventId); - if (pod.type == EventRelation::Annotation()) - fromJson(jo["key"_ls], pod.key); -} diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index ce11eaed..b3cb3ca7 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -4,38 +4,10 @@ #pragma once #include "roomevent.h" +#include "eventrelation.h" namespace Quotient { -struct QUOTIENT_API EventRelation { - using reltypeid_t = const char*; - static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } - static constexpr reltypeid_t Annotation() { return "m.annotation"; } - static constexpr reltypeid_t Replacement() { return "m.replace"; } - - QString type; - QString eventId; - QString key = {}; // Only used for m.annotation for now - - static EventRelation replyTo(QString eventId) - { - return { Reply(), std::move(eventId) }; - } - static EventRelation annotate(QString eventId, QString key) - { - return { Annotation(), std::move(eventId), std::move(key) }; - } - static EventRelation replace(QString eventId) - { - return { Replacement(), std::move(eventId) }; - } -}; -template <> -struct QUOTIENT_API JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const EventRelation& pod); - static void fillFrom(const QJsonObject& jo, EventRelation& pod); -}; - class QUOTIENT_API ReactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) @@ -47,7 +19,7 @@ public: explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} EventRelation relation() const { - return contentPart("m.relates_to"_ls); + return contentPart(RelatesToKey); } }; REGISTER_EVENT_TYPE(ReactionEvent) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 5ab0f845..c07a4f3c 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -6,6 +6,7 @@ #include "roommessageevent.h" #include "logging.h" +#include "events/eventrelation.h" #include #include @@ -20,7 +21,6 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; namespace { // Supporting internal definitions - constexpr auto RelatesToKey = "m.relates_to"_ls; constexpr auto MsgTypeKey = "msgtype"_ls; constexpr auto FormattedBodyKey = "formatted_body"_ls; @@ -84,9 +84,9 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline bool isReplacement(const Omittable& rel) +inline bool isReplacement(const Omittable& rel) { - return rel && rel->type == RelatesTo::ReplacementTypeId(); + return rel && rel->type == EventRelation::ReplacementType; } } // anonymous namespace @@ -105,10 +105,10 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, << "messages; the relation has been stripped off"; } else { // After the above, we know for sure that the content is TextContent - // and that its RelatesTo structure is not omitted + // and that its EventRelation structure is not omitted auto* textContent = static_cast(content); Q_ASSERT(textContent && textContent->relatesTo.has_value()); - if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { + if (textContent->relatesTo->type == EventRelation::ReplacementType) { auto newContentJson = json.take("m.new_content"_ls).toObject(); newContentJson.insert(BodyKey, plainBody); newContentJson.insert(MsgTypeKey, jsonMsgType); @@ -269,7 +269,7 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) } TextContent::TextContent(QString text, const QString& contentType, - Omittable relatesTo) + Omittable relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)) , body(std::move(text)) , relatesTo(std::move(relatesTo)) @@ -278,26 +278,8 @@ TextContent::TextContent(QString text, const QString& contentType, mimeType = QMimeDatabase().mimeTypeForName("text/html"); } -namespace Quotient { -// Overload the default fromJson<> logic that defined in converters.h -// as we want -template <> -Omittable fromJson(const QJsonValue& jv) -{ - const auto jo = jv.toObject(); - if (jo.isEmpty()) - return none; - const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject(); - if (!replyJson.isEmpty()) - return replyTo(fromJson(replyJson[EventIdKeyL])); - - return RelatesTo { jo.value("rel_type"_ls).toString(), - jo.value(EventIdKeyL).toString() }; -} -} // namespace Quotient - TextContent::TextContent(const QJsonObject& json) - : relatesTo(fromJson>(json[RelatesToKey])) + : relatesTo(fromJson>(json[RelatesToKey])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); @@ -331,13 +313,13 @@ void TextContent::fillJson(QJsonObject* json) const if (relatesTo) { json->insert( QStringLiteral("m.relates_to"), - relatesTo->type == RelatesTo::ReplyTypeId() + relatesTo->type == EventRelation::ReplyType ? QJsonObject { { relatesTo->type, QJsonObject { { EventIdKey, relatesTo->eventId } } } } - : QJsonObject { { "rel_type", relatesTo->type }, + : QJsonObject { { RelTypeKey, relatesTo->type }, { EventIdKey, relatesTo->eventId } }); - if (relatesTo->type == RelatesTo::ReplacementTypeId()) { + if (relatesTo->type == EventRelation::ReplacementType) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { newContentJson.insert(FormatKey, HtmlContentTypeId); diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 0c901b7a..44ef05fb 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -6,6 +6,7 @@ #pragma once #include "eventcontent.h" +#include "eventrelation.h" #include "roomevent.h" class QFileInfo; @@ -97,23 +98,25 @@ REGISTER_EVENT_TYPE(RoomMessageEvent) using MessageEventType = RoomMessageEvent::MsgType; namespace EventContent { - // Additional event content types - struct RelatesTo { - static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } - static constexpr const char* ReplacementTypeId() { return "m.replace"; } - QString type; // The only supported relation so far - QString eventId; + struct [[deprecated("Use Quotient::EventRelation instead")]] RelatesTo + : EventRelation { + static constexpr auto ReplyTypeId() { return Reply(); } + static constexpr auto ReplacementTypeId() { return Replacement(); } }; - inline RelatesTo replyTo(QString eventId) + [[deprecated("Use EventRelation::replyTo() instead")]] + inline auto replyTo(QString eventId) { - return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + return EventRelation::replyTo(std::move(eventId)); } - inline RelatesTo replacementOf(QString eventId) + [[deprecated("Use EventRelation::replace() instead")]] + inline auto replacementOf(QString eventId) { - return { RelatesTo::ReplacementTypeId(), std::move(eventId) }; + return EventRelation::replace(std::move(eventId)); } + // Additional event content types + /** * Rich text content for m.text, m.emote, m.notice * @@ -123,14 +126,14 @@ namespace EventContent { class QUOTIENT_API TextContent : public TypedBase { public: TextContent(QString text, const QString& contentType, - Omittable relatesTo = none); + Omittable relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; - Omittable relatesTo; + Omittable relatesTo; protected: void fillJson(QJsonObject* json) const override; 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); diff --git a/lib/room.h b/lib/room.h index 61f475e8..15bc7648 100644 --- a/lib/room.h +++ b/lib/room.h @@ -21,6 +21,7 @@ #include "events/roommessageevent.h" #include "events/roomcreateevent.h" #include "events/roomtombstoneevent.h" +#include "events/eventrelation.h" #include #include @@ -394,9 +395,9 @@ public: PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; const RelatedEvents relatedEvents(const QString& evtId, - const char* relType) const; + EventRelation::reltypeid_t relType) const; const RelatedEvents relatedEvents(const RoomEvent& evt, - const char* relType) const; + EventRelation::reltypeid_t relType) const; const RoomCreateEvent* creation() const { diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 5a5642fe..40bc456b 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -381,7 +381,7 @@ TEST_IMPL(sendReaction) if (actualTargetEvtId != targetEvtId) return false; const auto reactions = targetRoom->relatedEvents( - targetEvtId, EventRelation::Annotation()); + targetEvtId, EventRelation::AnnotationType); // It's a test room, assuming no interference there should // be exactly one reaction if (reactions.size() != 1) -- cgit v1.2.3 From 0689028f4a0db403a55c6158e750fee3ba6c7098 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 20 Jan 2022 17:35:28 +0100 Subject: Refactor assembleContentJson() Get rid of that Q_ASSERT() in the middle that only worked in Debug builds anyway. --- lib/events/roommessageevent.cpp | 43 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index c07a4f3c..d63352cb 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -95,32 +95,33 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { - auto json = content ? content->toJson() : QJsonObject(); - if (json.contains(RelatesToKey)) { + QJsonObject json; + if (content) { + // TODO: replace with content->fillJson(json) when it starts working + json = content->toJson(); if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && jsonMsgType != EmoteTypeKey) { - json.remove(RelatesToKey); - qCWarning(EVENTS) - << RelatesToKey << "cannot be used in" << jsonMsgType - << "messages; the relation has been stripped off"; - } else { - // After the above, we know for sure that the content is TextContent - // and that its EventRelation structure is not omitted - auto* textContent = static_cast(content); - Q_ASSERT(textContent && textContent->relatesTo.has_value()); - if (textContent->relatesTo->type == EventRelation::ReplacementType) { - auto newContentJson = json.take("m.new_content"_ls).toObject(); - newContentJson.insert(BodyKey, plainBody); - newContentJson.insert(MsgTypeKey, jsonMsgType); - json.insert(QStringLiteral("m.new_content"), newContentJson); - json[MsgTypeKey] = jsonMsgType; - json[BodyKeyL] = "* " + plainBody; - return json; + if (json.contains(RelatesToKey)) { + json.remove(RelatesToKey); + qCWarning(EVENTS) + << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; } + } else if (auto* textContent = static_cast(content); + textContent->relatesTo + && textContent->relatesTo->type + == EventRelation::ReplacementType) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKey, plainBody); + newContentJson.insert(MsgTypeKey, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[MsgTypeKey] = jsonMsgType; + json[BodyKeyL] = "* " + plainBody; + return json; } } - json.insert(QStringLiteral("msgtype"), jsonMsgType); - json.insert(QStringLiteral("body"), plainBody); + json.insert(MsgTypeKey, jsonMsgType); + json.insert(BodyKey, plainBody); return json; } -- cgit v1.2.3 From 63c953800330017ebb2afbabf41e5c4932c4d640 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 22 Jan 2022 13:43:15 +0100 Subject: Quotest: fix changeName test This test was very unreliable because memberRenamed() signal was emitted upon lazy-loading of past member renames. The new code connects to aboutToAddNewMessages() instead, which should only contain the latest renaming event (past events are delivered in the 'state' object). --- quotest/quotest.cpp | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 40bc456b..792faabd 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -14,6 +14,7 @@ #include "events/reactionevent.h" #include "events/redactionevent.h" #include "events/simplestateevents.h" +#include "events/roommemberevent.h" #include #include @@ -592,25 +593,31 @@ TEST_IMPL(changeName) clog << "Renaming the user to " << newName.toStdString() << " in the target room" << endl; localUser->rename(newName, targetRoom); - connectUntil(targetRoom, &Room::memberRenamed, this, - [this, thisTest, localUser, newName](const User* u) { - if (localUser != u) - return false; - if (localUser->name(targetRoom) != newName) - FAIL_TEST(); - - clog - << "Member rename successful, renaming the account" + connectUntil( + targetRoom, &Room::aboutToAddNewMessages, this, + [this, thisTest, localUser, newName](const RoomEventsRange& evts) { + for (const auto& e : evts) { + if (const auto* rme = eventCast(e)) { + if (rme->stateKey() != localUser->id() + || !rme->isRename()) + continue; + if (!rme->newDisplayName() + || *rme->newDisplayName() != newName) + FAIL_TEST(); + clog << "Member rename successful, renaming the account" << endl; - const auto newN = newName.mid(0, 5); - localUser->rename(newN); - connectUntil(localUser, &User::defaultNameChanged, - this, [this, thisTest, localUser, newN] { - targetRoom->localUser()->rename({}); - FINISH_TEST(localUser->name() == newN); - }); - return true; - }); + const auto newN = newName.mid(0, 5); + localUser->rename(newN); + connectUntil(localUser, &User::defaultNameChanged, this, + [this, thisTest, localUser, newN] { + targetRoom->localUser()->rename({}); + FINISH_TEST(localUser->name() == newN); + }); + return true; + } + } + return false; + }); }); return false; } -- cgit v1.2.3 From cc9908e5159ed93a18eda9f9794a7c9fc7f67f27 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 23 Jan 2022 10:34:38 +0100 Subject: Fix visit() return type It's too restrictive compared to switchOnType() overloads and doesn't map to the case with a default value. --- lib/events/event.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index f10f6a8d..113fa3fa 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -354,8 +354,7 @@ switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) template [[deprecated("The new name for visit() is switchOnType()")]] // -inline std::common_type_t...> -visit(const BaseT& event, FnTs&&... fns) +inline auto visit(const BaseT& event, FnTs&&... fns) { return switchOnType(event, std::forward(fns)...); } -- cgit v1.2.3 From abbab8d8f8c566bc2c9cdf766c6fbb11d978ca47 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 23 Jan 2022 17:11:22 +0100 Subject: Omittable: split out from util.h and refresh Improvements: - Quotient::lift() - a way to invoke a function on an optional (including Omittable) or a pointer if it's 'truthy'. Doesn't need enhanced function_traits<>, only the standard library; works on any number of arguments that can be dereferenced and casted to bool. - then() - the version of lift() as a member function. - edit() was renamed to ensure() (edit() might become a read-write counterpart of then() at some point). It's not really used across libQuotient codebase (or elsewhere) but is staying there just in case. It can also accept an initializer, removing the requirement of default-constructibility. - Quotient::merge() is simplified, with one universal implementation covering both Omittable/optional and plain values. - All that now lives in its dedicated pair of files, further decluttering util.h --- CMakeLists.txt | 1 + gtad/gtad.yaml | 2 +- lib/connection.h | 1 + lib/converters.h | 1 + lib/omittable.cpp | 34 +++++++++ lib/omittable.h | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/util.h | 141 ---------------------------------- 7 files changed, 261 insertions(+), 142 deletions(-) create mode 100644 lib/omittable.cpp create mode 100644 lib/omittable.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 599424ab..13c8f591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ list(APPEND lib_SRCS lib/quotient_common.h lib/quotient_export.h lib/function_traits.h lib/function_traits.cpp + lib/omittable.h lib/omittable.cpp lib/networkaccessmanager.h lib/networkaccessmanager.cpp lib/connectiondata.h lib/connectiondata.cpp lib/connection.h lib/connection.cpp diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 943ac013..03c23886 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -45,7 +45,7 @@ analyzer: types: - +set: &UseOmittable useOmittable: - omittedValue: 'none' # Quotient::none in lib/util.h + omittedValue: 'none' # Quotient::none in lib/omittable.h +on: - integer: - int64: qint64 diff --git a/lib/connection.h b/lib/connection.h index 28688cc1..dc2eaad1 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -8,6 +8,7 @@ #include "ssosession.h" #include "qt_connection_util.h" #include "quotient_common.h" +#include "util.h" #include "csapi/login.h" #include "csapi/create_room.h" diff --git a/lib/converters.h b/lib/converters.h index 8eea1cd3..e7bc6898 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -3,6 +3,7 @@ #pragma once +#include "omittable.h" #include "util.h" #include diff --git a/lib/omittable.cpp b/lib/omittable.cpp new file mode 100644 index 00000000..245ae721 --- /dev/null +++ b/lib/omittable.cpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "omittable.h" + +// Omittable<> tests +using namespace Quotient; + +Omittable testFn(bool) { return 0; } +bool testFn2(int) { return false; } +static_assert( + std::is_same_v>().then(testFn)), + Omittable>); +static_assert( + std::is_same_v< + decltype(std::declval>().then_or(testFn, 0)), int>); +static_assert( + std::is_same_v>().then(testFn)), + Omittable>); +static_assert(std::is_same_v>() + .then(testFn2) + .then(testFn)), + Omittable>); +static_assert(std::is_same_v>() + .then(testFn) + .then_or(testFn2, false)), + bool>); + +constexpr auto visitTestFn(int, bool) { return false; } +static_assert( + std::is_same_v, decltype(lift(testFn2, Omittable()))>); +static_assert(std::is_same_v, + decltype(lift(visitTestFn, Omittable(), + Omittable()))>); diff --git a/lib/omittable.h b/lib/omittable.h new file mode 100644 index 00000000..b5efecf5 --- /dev/null +++ b/lib/omittable.h @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +namespace Quotient { + +template +class Omittable; + +constexpr auto none = std::nullopt; + +//! \brief Lift an operation into dereferenceable types (Omittables or pointers) +//! +//! This is a more generic version of Omittable::then() that extends to +//! an arbitrary number of arguments of any type that is dereferenceable (unary +//! operator*() can be applied to it) and (explicitly or implicitly) convertible +//! to bool. This allows to streamline checking for nullptr/none before applying +//! the operation on the underlying types. \p fn is only invoked if all \p args +//! are "truthy" (i.e. (... && bool(args)) == true). +//! \param fn A callable that should accept the types stored inside +//! Omittables/pointers passed in \p args +//! \return Always an Omittable: if \p fn returns another type, lift() wraps +//! it in an Omittable; if \p fn returns an Omittable, that return value +//! (or none) is returned as is. +template +inline auto lift(FnT&& fn, MaybeTs&&... args) +{ + return (... && bool(args)) + ? Omittable(std::invoke(std::forward(fn), *args...)) + : none; +} + +/** `std::optional` with tweaks + * + * The tweaks are: + * - streamlined assignment (operator=)/emplace()ment of values that can be + * used to implicitly construct the underlying type, including + * direct-list-initialisation, e.g.: + * \code + * struct S { int a; char b; } + * Omittable o; + * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } + * \endcode + * - entirely deleted value(). The technical reason is that Xcode 10 doesn't + * have it; but besides that, value_or() or (after explicit checking) + * `operator*()`/`operator->()` are better alternatives within Quotient + * that doesn't practice throwing exceptions (as doesn't most of Qt). + * - disabled non-const lvalue operator*() and operator->(), as it's too easy + * to inadvertently cause a value change through them. + * - ensure() to provide a safe and explicit lvalue accessor instead of + * those above. Allows chained initialisation of nested Omittables: + * \code + * struct Inner { int member = 10; Omittable innermost; }; + * struct Outer { int anotherMember = 10; Omittable inner; }; + * Omittable o; // = { 10, std::nullopt }; + * o.ensure().inner.ensure().innermost.emplace(42); + * \endcode + * - merge() - a soft version of operator= that only overwrites its first + * operand with the second one if the second one is not empty. + * - then() and then_or() to streamline read-only interrogation in a "monadic" + * interface. + */ +template +class Omittable : public std::optional { +public: + using base_type = std::optional; + using value_type = std::decay_t; + + using std::optional::optional; + + // Overload emplace() and operator=() to allow passing braced-init-lists + // (the standard emplace() does direct-initialisation but + // not direct-list-initialisation). + using base_type::operator=; + Omittable& operator=(const value_type& v) + { + base_type::operator=(v); + return *this; + } + Omittable& operator=(value_type&& v) + { + base_type::operator=(std::move(v)); + return *this; + } + + using base_type::emplace; + T& emplace(const T& val) { return base_type::emplace(val); } + T& emplace(T&& val) { return base_type::emplace(std::move(val)); } + + // Use value_or() or check (with operator! or has_value) before accessing + // with operator-> or operator* + // The technical reason is that Xcode 10 has incomplete std::optional + // that has no value(); but using value() may also mean that you rely + // on the optional throwing an exception (which is not assumed practice + // throughout Quotient) or that you spend unnecessary CPU cycles on + // an extraneous has_value() check. + auto& value() = delete; + const auto& value() const = delete; + + template + value_type& ensure(U&& defaultValue = value_type {}) + { + return this->has_value() ? this->operator*() + : this->emplace(std::forward(defaultValue)); + } + value_type& ensure(const value_type& defaultValue) + { + return ensure<>(defaultValue); + } + value_type& ensure(value_type&& defaultValue) + { + return ensure<>(std::move(defaultValue)); + } + + //! Merge the value from another Omittable + //! \return true if \p other is not omitted and the value of + //! the current Omittable was different (or omitted), + //! in other words, if the current Omittable has changed; + //! false otherwise + template + auto merge(const std::optional& other) + -> std::enable_if_t, bool> + { + if (!other || (this->has_value() && **this == *other)) + return false; + this->emplace(*other); + return true; + } + + // Hide non-const lvalue operator-> and operator* as these are + // a bit too surprising: value() & doesn't lazy-create an object; + // and it's too easy to inadvertently change the underlying value. + + const value_type* operator->() const& { return base_type::operator->(); } + value_type* operator->() && { return base_type::operator->(); } + const value_type& operator*() const& { return base_type::operator*(); } + value_type& operator*() && { return base_type::operator*(); } + + // The below is inspired by the proposed std::optional monadic operations + // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html). + + //! \brief Lift a callable into the Omittable + //! + //! 'Lifting', as used in functional programming, means here invoking + //! a callable (e.g., a function) on the contents of the Omittable if it has + //! any and wrapping the returned value (that may be of a different type T2) + //! into a new Omittable\. If the current Omittable is empty, + //! the invocation is skipped altogether and Omittable\{none} is + //! returned instead. + //! \note if \p fn already returns an Omittable (i.e., it is a 'functor', + //! in functional programming terms), then() will not wrap another + //! Omittable around but will just return what \p fn returns. The + //! same doesn't hold for the parameter: if \p fn accepts an Omittable + //! you have to wrap it in another Omittable before calling then(). + //! \return `none` if the current Omittable has `none`; + //! otherwise, the Omittable returned from a call to \p fn + //! \tparam FnT a callable with \p T (or const T&) + //! returning Omittable, T2 is any supported type + //! \sa then_or, transform + template + auto then(FnT&& fn) const& + { + return lift(std::forward(fn), *this); + } + + //! \brief Lift a callable into the rvalue Omittable + //! + //! This is an rvalue overload for then(). + template + auto then(FnT&& fn) && + { + return lift(std::forward(fn), *this); + } + + //! \brief Lift a callable into the const lvalue Omittable, with a fallback + //! + //! This effectively does the same what then() does, except that it returns + //! a value of type returned by the callable, or the provided fallback value + //! if the current Omittable is empty. This is a typesafe version to apply + //! an operation on an Omittable without having to deal with another + //! Omittable afterwards. + template + auto then_or(FnT&& fn, FallbackT&& fallback) const& + { + return then(std::forward(fn)) + .value_or(std::forward(fallback)); + } + + //! \brief Lift a callable into the rvalue Omittable, with a fallback + //! + //! This is an overload for functions that accept rvalue + template + auto then_or(FnT&& fn, FallbackT&& fallback) && + { + return then(std::forward(fn)) + .value_or(std::forward(fallback)); + } +}; + +template +Omittable(T&&) -> Omittable; + +//! \brief Merge the value from an optional +//! This is an adaptation of Omittable::merge() to the case when the value +//! on the left hand side is not an Omittable. +//! \return true if \p rhs is not omitted and the \p lhs value was different, +//! in other words, if \p lhs has changed; +//! false otherwise +template +inline auto merge(T1& lhs, const std::optional& rhs) + -> std::enable_if_t, bool> +{ + if (!rhs || lhs == *rhs) + return false; + lhs = *rhs; + return true; +} + +} // namespace Quotient diff --git a/lib/util.h b/lib/util.h index 3505b62f..753eb1ea 100644 --- a/lib/util.h +++ b/lib/util.h @@ -9,10 +9,8 @@ #include #include -#include #include #include -#include #ifndef Q_DISABLE_MOVE // Q_DISABLE_MOVE was introduced in Q_VERSION_CHECK(5,13,0) @@ -52,145 +50,6 @@ struct HashQ { template using UnorderedMap = std::unordered_map>; -namespace _impl { - template - constexpr auto IsOmittableValue = false; - template - constexpr auto IsOmittable = IsOmittableValue>; -} - -constexpr auto none = std::nullopt; - -/** `std::optional` with tweaks - * - * The tweaks are: - * - streamlined assignment (operator=)/emplace()ment of values that can be - * used to implicitly construct the underlying type, including - * direct-list-initialisation, e.g.: - * \code - * struct S { int a; char b; } - * Omittable o; - * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } - * \endcode - * - entirely deleted value(). The technical reason is that Xcode 10 doesn't - * have it; but besides that, value_or() or (after explicit checking) - * `operator*()`/`operator->()` are better alternatives within Quotient - * that doesn't practice throwing exceptions (as doesn't most of Qt). - * - disabled non-const lvalue operator*() and operator->(), as it's too easy - * to inadvertently cause a value change through them. - * - edit() to provide a safe and explicit lvalue accessor instead of those - * above. Requires the underlying type to be default-constructible. - * Allows chained initialisation of nested Omittables: - * \code - * struct Inner { int member = 10; Omittable innermost; }; - * struct Outer { int anotherMember = 10; Omittable inner; }; - * Omittable o; // = { 10, std::nullopt }; - * o.edit().inner.edit().innermost.emplace(42); - * \endcode - * - merge() - a soft version of operator= that only overwrites its first - * operand with the second one if the second one is not empty. - */ -template -class Omittable : public std::optional { -public: - using base_type = std::optional; - using value_type = std::decay_t; - - using std::optional::optional; - - // Overload emplace() and operator=() to allow passing braced-init-lists - // (the standard emplace() does direct-initialisation but - // not direct-list-initialisation). - using base_type::operator=; - Omittable& operator=(const value_type& v) - { - base_type::operator=(v); - return *this; - } - Omittable& operator=(value_type&& v) - { - base_type::operator=(v); - return *this; - } - using base_type::emplace; - T& emplace(const T& val) { return base_type::emplace(val); } - T& emplace(T&& val) { return base_type::emplace(std::move(val)); } - - // use value_or() or check (with operator! or has_value) before accessing - // with operator-> or operator* - // The technical reason is that Xcode 10 has incomplete std::optional - // that has no value(); but using value() may also mean that you rely - // on the optional throwing an exception (which is not assumed practice - // throughout Quotient) or that you spend unnecessary CPU cycles on - // an extraneous has_value() check. - value_type& value() = delete; - const value_type& value() const = delete; - value_type& edit() - { - return this->has_value() ? base_type::operator*() : this->emplace(); - } - - [[deprecated("Use '!o' or '!o.has_value()' instead of 'o.omitted()'")]] - bool omitted() const - { - return !this->has_value(); - } - - //! Merge the value from another Omittable - //! \return true if \p other is not omitted and the value of - //! the current Omittable was different (or omitted), - //! in other words, if the current Omittable has changed; - //! false otherwise - template - auto merge(const Omittable& other) - -> std::enable_if_t, bool> - { - if (!other || (this->has_value() && **this == *other)) - return false; - emplace(*other); - return true; - } - - // Hide non-const lvalue operator-> and operator* as these are - // a bit too surprising: value() & doesn't lazy-create an object; - // and it's too easy to inadvertently change the underlying value. - - const value_type* operator->() const& { return base_type::operator->(); } - value_type* operator->() && { return base_type::operator->(); } - const value_type& operator*() const& { return base_type::operator*(); } - value_type& operator*() && { return base_type::operator*(); } -}; -template -Omittable(T&&) -> Omittable; - -namespace _impl { - template - constexpr auto IsOmittableValue> = true; -} - -template -inline auto merge(Omittable& lhs, T2&& rhs) -{ - return lhs.merge(std::forward(rhs)); -} - -//! \brief Merge the value from an Omittable -//! This is an adaptation of Omittable::merge() to the case when the value -//! on the left hand side is not an Omittable. -//! \return true if \p rhs is not omitted and the \p lhs value was different, -//! in other words, if \p lhs has changed; -//! false otherwise -template -inline auto merge(T1& lhs, const Omittable& rhs) - -> std::enable_if_t - && std::is_convertible_v, bool> -{ - if (!rhs || lhs == *rhs) - return false; - lhs = *rhs; - return true; -} - constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); -- cgit v1.2.3 From 29775218e0c8b6c176015dd3128a0d545906ae6f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 2 Dec 2021 20:54:39 +0100 Subject: Cleanup some #includes --- lib/events/eventcontent.cpp | 1 - lib/events/receiptevent.cpp | 1 - lib/events/roomevent.cpp | 1 - lib/events/roommemberevent.cpp | 1 - lib/jobs/basejob.cpp | 2 -- lib/jobs/basejob.h | 6 +++--- 6 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 22878d4c..4ce130a6 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -4,7 +4,6 @@ #include "eventcontent.h" #include "converters.h" -#include "util.h" #include "logging.h" #include diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 72dbf2e3..7f06d99f 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -20,7 +20,6 @@ Example of a Receipt Event: #include "receiptevent.h" -#include "converters.h" #include "logging.h" using namespace Quotient; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index b728e0bf..3502e3f7 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -3,7 +3,6 @@ #include "roomevent.h" -#include "converters.h" #include "logging.h" #include "redactionevent.h" diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 3141f6b5..b4770224 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -4,7 +4,6 @@ #include "roommemberevent.h" -#include "converters.h" #include "logging.h" #include diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index f518a1b0..b6858b5a 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -5,11 +5,9 @@ #include "basejob.h" #include "connectiondata.h" -#include "quotient_common.h" #include #include -#include #include #include #include diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 9ed58ba8..555c602b 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -5,9 +5,9 @@ #pragma once #include "requestdata.h" -#include "../logging.h" -#include "../converters.h" -#include "../quotient_common.h" +#include "logging.h" +#include "converters.h" // Common for csapi/ headers even though not used here +#include "quotient_common.h" // For DECL_DEPRECATED_ENUMERATOR #include #include -- cgit v1.2.3 From 1215888438b901ad80e8bf9882cdcc6a3f057399 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 4 Dec 2021 20:12:59 +0100 Subject: converters.h: slightly clearer code in _impl::addTo --- lib/converters.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index e7bc6898..8ddf6e45 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -288,6 +288,8 @@ QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject +constexpr bool IfNotEmpty = false; + namespace _impl { template inline void addTo(QJsonObject& o, const QString& k, ValT&& v) @@ -333,7 +335,7 @@ namespace _impl { // This one is for types that have isEmpty() when Force is false template - struct AddNode().isEmpty())> { + struct AddNode().isEmpty())> { template static void impl(ContT& container, const QString& key, ForwardedT&& value) @@ -343,9 +345,9 @@ namespace _impl { } }; - // This one unfolds Omittable<> (also only when Force is false) + // This one unfolds Omittable<> (also only when IfNotEmpty is requested) template - struct AddNode, false> { + struct AddNode, IfNotEmpty> { template static void impl(ContT& container, const QString& key, const OmittableT& value) @@ -356,8 +358,6 @@ namespace _impl { }; } // namespace _impl -constexpr bool IfNotEmpty = false; - /*! Add a key-value pair to QJsonObject or QUrlQuery * * Adds a key-value pair(s) specified by \p key and \p value to -- 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. --- CMakeLists.txt | 1 + lib/room.cpp | 150 ++++++++++++++++++++++++-------------------------- lib/room.h | 54 ++++++++---------- lib/roomstateview.cpp | 35 ++++++++++++ lib/roomstateview.h | 127 ++++++++++++++++++++++++++++++++++++++++++ lib/user.cpp | 17 ++++-- 6 files changed, 268 insertions(+), 116 deletions(-) create mode 100644 lib/roomstateview.cpp create mode 100644 lib/roomstateview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 13c8f591..ddf11680 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,7 @@ list(APPEND lib_SRCS lib/ssosession.h lib/ssosession.cpp lib/logging.h lib/logging.cpp lib/room.h lib/room.cpp + lib/roomstateview.h lib/roomstateview.cpp lib/user.h lib/user.cpp lib/avatar.h lib/avatar.cpp lib/uri.h lib/uri.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 diff --git a/lib/room.h b/lib/room.h index 15bc7648..d49cfb55 100644 --- a/lib/room.h +++ b/lib/room.h @@ -10,6 +10,7 @@ #pragma once #include "connection.h" +#include "roomstateview.h" #include "eventitem.h" #include "quotient_common.h" @@ -399,14 +400,8 @@ public: const RelatedEvents relatedEvents(const RoomEvent& evt, EventRelation::reltypeid_t relType) const; - const RoomCreateEvent* creation() const - { - return getCurrentState(); - } - const RoomTombstoneEvent* tombstone() const - { - return getCurrentState(); - } + const RoomCreateEvent* creation() const; + const RoomTombstoneEvent* tombstone() const; bool displayed() const; /// Mark the room as currently displayed to the user @@ -760,45 +755,40 @@ public: /*! This method returns a (potentially empty) state event corresponding * to the pair of event type \p evtType and state key \p stateKey. */ - Q_INVOKABLE const Quotient::StateEventBase* + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // + const Quotient::StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; - /// Get all state events in the room. - /*! This method returns all known state events that have occured in - * the room, as a mapping from the event type and state key to value. - */ - const QHash& currentState() const; - - /// Get all state events in the room of a certain type. - /*! This method returns all known state events that have occured in - * the room of the given type. - */ - Q_INVOKABLE const QVector - stateEventsOfType(const QString& evtType) const; - /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of * its Matrix name. */ template + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // const EvT* getCurrentState(const QString& stateKey = {}) const { - const auto* evt = - eventCast(getCurrentState(EvT::matrixTypeId(), stateKey)); + QT_IGNORE_DEPRECATIONS( + const auto* evt = eventCast( + getCurrentState(EvT::matrixTypeId(), stateKey));) Q_ASSERT(evt); Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() && evt->stateKey() == stateKey); return evt; } - /// Set a state event of the given type with the given arguments - /*! This typesafe overload attempts to send a state event with the type - * \p EvT and the content defined by \p args. Specifically, the function - * creates a temporary object of type \p EvT passing \p args to - * the constructor, and sends a request to the homeserver using - * the Matrix event type defined by \p EvT and the event content produced - * via EvT::contentJson(). - */ + /// \brief Get the current room state + RoomStateView currentState() const; + + //! \brief Set a state event of the given type with the given arguments + //! + //! This typesafe overload attempts to send a state event with the type + //! \p EvT and the content defined by \p args. Specifically, the function + //! creates a temporary object of type \p EvT passing \p args to + //! the constructor, and sends a request to the homeserver using + //! the Matrix event type defined by \p EvT and the event content produced + //! via EvT::contentJson(). template auto setState(ArgTs&&... args) const { diff --git a/lib/roomstateview.cpp b/lib/roomstateview.cpp new file mode 100644 index 00000000..94c88eee --- /dev/null +++ b/lib/roomstateview.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomstateview.h" + +using namespace Quotient; + +const StateEventBase* RoomStateView::get(const QString& evtType, + const QString& stateKey) const +{ + return value({ evtType, stateKey }); +} + +bool RoomStateView::contains(const QString& evtType, + const QString& stateKey) const +{ + return contains({ evtType, stateKey }); +} + +QJsonObject RoomStateView::contentJson(const QString& evtType, + const QString& stateKey) const +{ + return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject()); +} + +const QVector +RoomStateView::eventsOfType(const QString& evtType) const +{ + auto vals = QVector(); + for (auto it = cbegin(); it != cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + + return vals; +} diff --git a/lib/roomstateview.h b/lib/roomstateview.h new file mode 100644 index 00000000..cab69ae3 --- /dev/null +++ b/lib/roomstateview.h @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/stateevent.h" + +#include + +namespace Quotient { + +class Room; + +class RoomStateView : private QHash { + Q_GADGET +public: + const QHash& events() const + { + return *this; + } + + //! \brief Get a state event with the given event type and state key + //! \return A state event corresponding to the pair of event type + //! \p evtType and state key \p stateKey, or nullptr if there's + //! no such \p evtType / \p stateKey combination in the current + //! state. + //! \warning In libQuotient 0.7 the return type changed to an OmittableCref + //! which is effectively a nullable const reference wrapper. You + //! have to check that it has_value() before using. Alternatively + //! you can now use queryCurrentState() to access state safely. + //! \sa getCurrentStateContentJson + const StateEventBase* get(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get a state event with the given event type and state key + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. + //! \warning In libQuotient 0.7 the return type changed to an Omittable with + //! a reference wrapper inside - you have to check that it + //! has_value() before using. Alternatively you can now use + //! queryCurrentState() to access state safely. + template + const EvT* get(const QString& stateKey = {}) const + { + static_assert(std::is_base_of_v); + if (const auto* evt = get(EvT::matrixTypeId(), stateKey)) { + Q_ASSERT(evt->matrixType() == EvT::matrixTypeId() + && evt->stateKey() == stateKey); + return eventCast(evt); + } + return nullptr; + } + + using QHash::contains; + + bool contains(const QString& evtType, const QString& stateKey = {}) const; + + template + bool contains(const QString& stateKey = {}) const + { + return contains(EvT::matrixTypeId(), stateKey); + } + + //! \brief Get the content of the current state event with the given + //! event type and state key + //! \return An empty object if there's no event in the current state with + //! this event type and state key; the contents of the event + //! 'content' object otherwise + Q_INVOKABLE QJsonObject contentJson(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get all state events in the room of a certain type. + //! + //! This method returns all known state events that have occured in + //! the room of the given type. + const QVector + eventsOfType(const QString& evtType) const; + + template + auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const + { + return lift(std::forward(fn), get(evtType, stateKey)); + } + + template + auto query(const QString& stateKey, FnT&& fn) const + { + using EventT = std::decay_t>; + static_assert(std::is_base_of_v); + return lift(std::forward(fn), get(stateKey)); + } + + template + auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn, + FallbackT&& fallback) const + { + return lift(std::forward(fn), get(evtType, stateKey)) + .value_or(std::forward(fallback)); + } + + template + auto query(FnT&& fn) const + { + return query({}, std::forward(fn)); + } + + template + auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const + { + using EventT = std::decay_t>; + static_assert(std::is_base_of_v); + return lift(std::forward(fn), get(stateKey)) + .value_or(std::forward(fallback)); + } + + template + auto queryOr(FnT&& fn, FallbackT&& fallback) const + { + return queryOr({}, std::forward(fn), + std::forward(fallback)); + } + +private: + friend class Room; +}; +} // namespace Quotient diff --git a/lib/user.cpp b/lib/user.cpp index 0dbc444a..f7840c40 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -119,12 +119,17 @@ void User::rename(const QString& newName, const Room* r) return; } // #481: take the current state and update it with the new name - auto evtC = r->getCurrentState(id())->content(); - Q_ASSERT_X(evtC.membership == Membership::Join, __FUNCTION__, - "Attempt to rename a user that's not a room member"); - evtC.displayName = sanitized(newName); - r->setState(id(), move(evtC)); - // The state will be updated locally after it arrives with sync + if (const auto& maybeEvt = r->currentState().get(id())) { + auto content = maybeEvt->content(); + if (content.membership == Membership::Join) { + content.displayName = sanitized(newName); + r->setState(id(), move(content)); + // The state will be updated locally after it arrives with sync + return; + } + } + qCCritical(MEMBERS) + << "Attempt to rename a non-member in a room context - ignored"; } template -- cgit v1.2.3 From a1fcad591968ec717214a73a2dbe78f608207bc5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 5 Dec 2021 10:59:44 +0100 Subject: Move away Omittable static tests to autotests/ These are not required to build libQuotient, and omittable.cpp entirely consisted of them. --- CMakeLists.txt | 2 +- autotests/CMakeLists.txt | 1 + autotests/utiltests.cpp | 45 +++++++++++++++++++++++++++++++++++++++++++++ lib/omittable.cpp | 34 ---------------------------------- 4 files changed, 47 insertions(+), 35 deletions(-) create mode 100644 autotests/utiltests.cpp delete mode 100644 lib/omittable.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ddf11680..89eb996a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,7 +127,7 @@ list(APPEND lib_SRCS lib/quotient_common.h lib/quotient_export.h lib/function_traits.h lib/function_traits.cpp - lib/omittable.h lib/omittable.cpp + lib/omittable.h lib/networkaccessmanager.h lib/networkaccessmanager.cpp lib/connectiondata.h lib/connectiondata.cpp lib/connection.h lib/connection.cpp diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 282ab036..9efab0d1 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -12,3 +12,4 @@ function(QUOTIENT_ADD_TEST) endfunction() quotient_add_test(NAME callcandidateseventtest) +quotient_add_test(NAME utiltests) diff --git a/autotests/utiltests.cpp b/autotests/utiltests.cpp new file mode 100644 index 00000000..e3ec63d0 --- /dev/null +++ b/autotests/utiltests.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "omittable.h" + +#include + +// compile-time Omittable<> tests +using namespace Quotient; + +Omittable testFn(bool) { return 0; } +bool testFn2(int) { return false; } +static_assert( + std::is_same_v>().then(testFn)), + Omittable>); +static_assert( + std::is_same_v< + decltype(std::declval>().then_or(testFn, 0)), int>); +static_assert( + std::is_same_v>().then(testFn)), + Omittable>); +static_assert(std::is_same_v>() + .then(testFn2) + .then(testFn)), + Omittable>); +static_assert(std::is_same_v>() + .then(testFn) + .then_or(testFn2, false)), + bool>); + +constexpr auto visitTestFn(int, bool) { return false; } +static_assert( + std::is_same_v, decltype(lift(testFn2, Omittable()))>); +static_assert(std::is_same_v, + decltype(lift(visitTestFn, Omittable(), + Omittable()))>); + +class TestUtils : public QObject { + Q_OBJECT +private Q_SLOTS: + // TODO +}; + +QTEST_APPLESS_MAIN(TestUtils) +#include "utiltests.moc" diff --git a/lib/omittable.cpp b/lib/omittable.cpp deleted file mode 100644 index 245ae721..00000000 --- a/lib/omittable.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "omittable.h" - -// Omittable<> tests -using namespace Quotient; - -Omittable testFn(bool) { return 0; } -bool testFn2(int) { return false; } -static_assert( - std::is_same_v>().then(testFn)), - Omittable>); -static_assert( - std::is_same_v< - decltype(std::declval>().then_or(testFn, 0)), int>); -static_assert( - std::is_same_v>().then(testFn)), - Omittable>); -static_assert(std::is_same_v>() - .then(testFn2) - .then(testFn)), - Omittable>); -static_assert(std::is_same_v>() - .then(testFn) - .then_or(testFn2, false)), - bool>); - -constexpr auto visitTestFn(int, bool) { return false; } -static_assert( - std::is_same_v, decltype(lift(testFn2, Omittable()))>); -static_assert(std::is_same_v, - decltype(lift(visitTestFn, Omittable(), - Omittable()))>); -- 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 ++++++++++++++++++++---------------- lib/room.h | 11 ++++++++--- lib/user.cpp | 2 +- lib/user.h | 2 +- 4 files changed, 30 insertions(+), 21 deletions(-) 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) diff --git a/lib/room.h b/lib/room.h index d49cfb55..9f70d77a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -781,6 +781,9 @@ public: /// \brief Get the current room state RoomStateView currentState() const; + //! Send a request to update the room state with the given event + SetRoomStateWithKeyJob* setState(const StateEventBase& evt); + //! \brief Set a state event of the given type with the given arguments //! //! This typesafe overload attempts to send a state event with the type @@ -790,7 +793,7 @@ public: //! the Matrix event type defined by \p EvT and the event content produced //! via EvT::contentJson(). template - auto setState(ArgTs&&... args) const + auto setState(ArgTs&&... args) { return setState(EvT(std::forward(args)...)); } @@ -824,8 +827,10 @@ public Q_SLOTS: QString retryMessage(const QString& txnId); void discardMessage(const QString& txnId); - /// Send a request to update the room state with the given event - SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; + //! Send a request to update the room state based on freeform inputs + SetRoomStateWithKeyJob* setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); void setPinnedEvents(const QStringList& events); diff --git a/lib/user.cpp b/lib/user.cpp index f7840c40..4c3fc9e2 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -110,7 +110,7 @@ void User::rename(const QString& newName) }); } -void User::rename(const QString& newName, const Room* r) +void User::rename(const QString& newName, Room* r) { if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" diff --git a/lib/user.h b/lib/user.h index 8412b7fd..dfbff4a0 100644 --- a/lib/user.h +++ b/lib/user.h @@ -96,7 +96,7 @@ public Q_SLOTS: /// Set a new name in the global user profile void rename(const QString& newName); /// Set a new name for the user in one room - void rename(const QString& newName, const Room* r); + void rename(const QString& newName, Room* r); /// Upload the file and use it as an avatar bool setAvatar(const QString& fileName); /// Upload contents of the QIODevice and set that as an avatar -- cgit v1.2.3 From 08612cb253417fe70ef45a1ad08663a0745d748a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 10 Dec 2021 19:26:23 +0100 Subject: No more default construction of events Default construction was only done to support stubbed state in Room and even that did not really use those, opting to construct an event from an empty QJsonObject instead. Now that Room doesn't have stubbed state, default constructors are even less needed. --- lib/events/encryptionevent.h | 20 ++++++++++++-------- lib/events/roomcreateevent.h | 1 - lib/events/roommemberevent.h | 10 +++------- lib/events/roompowerlevelsevent.h | 4 +++- lib/events/roomtombstoneevent.h | 1 - lib/events/simplestateevents.h | 1 - 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index dfb28b2f..56913393 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -12,7 +12,11 @@ class QUOTIENT_API EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; - explicit EncryptionEventContent(EncryptionType et = Undefined); + explicit(false) EncryptionEventContent(EncryptionType et); + [[deprecated("This constructor will require explicit EncryptionType soon")]] // + explicit EncryptionEventContent() + : EncryptionEventContent(Undefined) + {} explicit EncryptionEventContent(const QJsonObject& json); EncryptionType encryption; @@ -34,15 +38,15 @@ public: using EncryptionType = EncryptionEventContent::EncryptionType; Q_ENUM(EncryptionType) - explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate - // default value + explicit EncryptionEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - EncryptionEvent(EncryptionEvent&&) = delete; - template - EncryptionEvent(ArgTs&&... contentArgs) - : StateEvent(typeId(), matrixTypeId(), QString(), - std::forward(contentArgs)...) + [[deprecated("This constructor will require an explicit parameter soon")]] // +// explicit EncryptionEvent() +// : EncryptionEvent(QJsonObject()) +// {} + explicit EncryptionEvent(EncryptionEventContent&& content) + : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) {} EncryptionType encryption() const { return content().encryption; } diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 016855b9..989030ac 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -11,7 +11,6 @@ class QUOTIENT_API RoomCreateEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) - explicit RoomCreateEvent() : StateEventBase(typeId(), matrixTypeId()) {} explicit RoomCreateEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) {} diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 5e446dbe..3296ae22 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -15,9 +15,7 @@ public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit MemberEventContent(Membership ms = Membership::Join) - : membership(ms) - {} + explicit(false) MemberEventContent(Membership ms) : membership(ms) {} explicit MemberEventContent(const QJsonObject& json); Membership membership; @@ -43,10 +41,8 @@ public: explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - template - RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) - : StateEvent(typeId(), matrixTypeId(), userId, - std::forward(contentArgs)...) + RoomMemberEvent(const QString& userId, MemberEventContent&& content) + : StateEvent(typeId(), matrixTypeId(), userId, std::move(content)) {} //! \brief A special constructor to create unknown RoomMemberEvents diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index 80e27048..415cc814 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -36,10 +36,12 @@ protected: class QUOTIENT_API RoomPowerLevelsEvent : public StateEvent { - Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent) + explicit RoomPowerLevelsEvent(PowerLevelsEventContent&& content) + : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) + {} explicit RoomPowerLevelsEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index e336c448..15d26923 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -10,7 +10,6 @@ class QUOTIENT_API RoomTombstoneEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) - explicit RoomTombstoneEvent() : StateEventBase(typeId(), matrixTypeId()) {} explicit RoomTombstoneEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) {} diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index e6c05880..9610574b 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -35,7 +35,6 @@ namespace EventContent { public: \ using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name() : _Name(value_type()) {} \ template \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), QString(), \ -- cgit v1.2.3 From 1747575321cda4fc11f90c2ffb2148a69d0d9946 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 28 Jan 2022 17:08:10 +0100 Subject: QUO_IMPLICIT Because Apple Clang choked on `explicit(false)`. --- lib/events/encryptionevent.h | 3 ++- lib/events/roommemberevent.h | 2 +- lib/quotient_common.h | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 56913393..124ced33 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -6,13 +6,14 @@ #include "eventcontent.h" #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { class QUOTIENT_API EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; - explicit(false) EncryptionEventContent(EncryptionType et); + QUO_IMPLICIT EncryptionEventContent(EncryptionType et); [[deprecated("This constructor will require explicit EncryptionType soon")]] // explicit EncryptionEventContent() : EncryptionEventContent(Undefined) diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 3296ae22..ceb7826b 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -15,7 +15,7 @@ public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit(false) MemberEventContent(Membership ms) : membership(ms) {} + QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {} explicit MemberEventContent(const QJsonObject& json); Membership membership; diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 02a9f0cd..b3fb3efa 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -25,6 +25,13 @@ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) +// Apple Clang hasn't caught up with explicit(bool) yet +#if __cpp_conditional_explicit >= 201806L +#define QUO_IMPLICIT explicit(false) +#else +#define QUO_IMPLICIT +#endif + #define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended -- cgit v1.2.3 From 350f40d07943a32319182bac6a21adf456642075 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 29 Jan 2022 16:12:22 +0100 Subject: SyncData: expect self-contained /sync response SyncData can load room objects out-of-line. This is only expected when loading data from the cache (and since quite long ago, the cache always saves room objects out of line, avoiding too large JSON payloads that Qt parser chokes on). However, the code processed /sync response in the same way; in particular, this meant that SyncData filled the vector of unresolved room ids even when it came from /sync. SyncJob then looked at this vector and entered an error state if it was not empty. Well, payloads from the wire can be weird and it ultimately came to pass that a homeserver returned a non-object against a given room key, triggering the unresolved rooms branch in SyncJob - and stalling the whole sync loop as a result (https://invent.kde.org/network/neochat/-/issues/500). With this commit SyncData only fills unresolvedRoomIds when loading rooms from the cache (with the implied fallback of discarding the cache and loading from /sync anew instead). Respectively, SyncJob must never end up with SyncData that has unresolved rooms (even if those occur in the actual payload like in the mentioned issue, those rooms will be completely empty instead); the added assertion only guards for internal consistency. --- lib/jobs/syncjob.cpp | 6 ++++-- lib/syncdata.cpp | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 9b1b46f0..f5c632bf 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -37,10 +37,12 @@ SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, BaseJob::Status SyncJob::prepareResult() { d.parseJson(jsonData()); - if (d.unresolvedRooms().isEmpty()) + if (Q_LIKELY(d.unresolvedRooms().isEmpty())) return Success; - qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + Q_ASSERT(d.unresolvedRooms().isEmpty()); + qCCritical(MAIN).noquote() << "Rooms missing after processing sync " + "response, possibly a bug in SyncData: " << d.unresolvedRooms().join(','); return IncorrectResponse; } diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 396e77eb..b0cd8e4d 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -185,14 +185,18 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) // We have a Qt container on the right and an STL one on the left roomData.reserve(roomData.size() + static_cast(rs.size())); for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { - auto roomJson = - roomIt->isObject() - ? roomIt->toObject() - : loadJson(baseDir + fileNameForRoom(roomIt.key())); - if (roomJson.isEmpty()) { - unresolvedRoomIds.push_back(roomIt.key()); - continue; - } + QJsonObject roomJson; + if (!baseDir.isEmpty()) { + // Loading data from the local cache, with room objects saved in + // individual files rather than inline + roomJson = loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + } else // When loading from /sync response, everything is inline + roomJson = roomIt->toObject(); + roomData.emplace_back(roomIt.key(), joinState, roomJson); const auto& r = roomData.back(); totalEvents += r.state.size() + r.ephemeral.size() -- cgit v1.2.3 From 0be5e13ce90c783ab49ae1f3223e9d84538b9112 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 29 Jan 2022 21:25:27 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- autotests/testolmaccount.cpp | 55 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index d547b683..45d158eb 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -171,14 +171,16 @@ void TestOlmAccount::encryptedFile() #define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ NetworkAccessManager::instance()->ignoreSslErrors(true); \ auto VAR = std::make_shared(); \ - (VAR) ->resolveServer("@alice:localhost:" + QString::number(443)); \ - connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [this, VAR ] () { \ + (VAR) ->resolveServer("@alice:localhost:443"); \ + connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [=] { \ (VAR) ->loginWithPassword( (USERNAME) , SECRET , DEVICE_NAME , ""); \ }); \ - connect( (VAR) .get(), &Connection::networkError, [=](QString error, const QString &, int, int) { \ + connect( (VAR) .get(), &Connection::networkError, [](QString error) { \ + QWARN(qUtf8Printable(error)); \ QFAIL("Network error: make sure synapse is running"); \ }); \ - connect( (VAR) .get(), &Connection::loginError, [=](QString error, const QString &) { \ + connect( (VAR) .get(), &Connection::loginError, [](QString error) { \ + QWARN(qUtf8Printable(error)); \ QFAIL("Login failed"); \ }); \ QSignalSpy spy ## VAR ((VAR).get(), &Connection::loginFlowsChanged); \ @@ -197,9 +199,8 @@ void TestOlmAccount::uploadIdentityKey() OneTimeKeys unused; auto request = olmAccount->createUploadKeyRequest(unused); - connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 0); + connect(request, &BaseJob::result, this, [request, conn] { + QCOMPARE(request->oneTimeKeyCounts().size(), 0); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -225,10 +226,9 @@ void TestOlmAccount::uploadOneTimeKeys() oneTimeKeysHash["curve25519:"+keyId] = key; } auto request = new UploadKeysJob(none, oneTimeKeysHash); - connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["curve25519"], 5); + connect(request, &BaseJob::result, this, [request, conn] { + QCOMPARE(request->oneTimeKeyCounts().size(), 1); + QCOMPARE(request->oneTimeKeyCounts()["curve25519"], 5); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -254,10 +254,9 @@ void TestOlmAccount::uploadSignedOneTimeKeys() oneTimeKeysHash[keyId] = var; } auto request = new UploadKeysJob(none, oneTimeKeysHash); - connect(request, &BaseJob::result, this, [request, nKeys, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], nKeys); + connect(request, &BaseJob::result, this, [request, nKeys, conn] { + QCOMPARE(request->oneTimeKeyCounts().size(), 1); + QCOMPARE(request->oneTimeKeyCounts()["signed_curve25519"], nKeys); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -275,10 +274,9 @@ void TestOlmAccount::uploadKeys() olmAccount->generateOneTimeKeys(1); auto otks = olmAccount->oneTimeKeys(); auto request = olmAccount->createUploadKeyRequest(otks); - connect(request, &BaseJob::result, this, [request, conn](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], 1); + connect(request, &BaseJob::result, this, [request, conn] { + QCOMPARE(request->oneTimeKeyCounts().size(), 1); + QCOMPARE(request->oneTimeKeyCounts()["signed_curve25519"], 1); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -309,7 +307,6 @@ void TestOlmAccount::queryTest() bobOlm->generateOneTimeKeys(1); auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); connect(bobRes, &BaseJob::result, this, [bobRes] { - QCOMPARE(bobRes->oneTimeKeyCounts().size(), 1); QCOMPARE(bobRes->oneTimeKeyCounts()["signed_curve25519"], 1); }); @@ -359,8 +356,6 @@ void TestOlmAccount::queryTest() } } - - void TestOlmAccount::claimKeys() { CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") @@ -371,10 +366,9 @@ void TestOlmAccount::claimKeys() bobOlm->generateOneTimeKeys(1); auto request = bobOlm->createUploadKeyRequest(bobOlm->oneTimeKeys()); - connect(request, &BaseJob::result, this, [request, bob](BaseJob *job) { - auto job2 = static_cast(job); - QCOMPARE(job2->oneTimeKeyCounts().size(), 1); - QCOMPARE(job2->oneTimeKeyCounts()["signed_curve25519"], 1); + connect(request, &BaseJob::result, this, [request, bob] { + QCOMPARE(request->oneTimeKeyCounts().size(), 1); + QCOMPARE(request->oneTimeKeyCounts()["signed_curve25519"], 1); }); bob->run(request); @@ -468,7 +462,6 @@ void TestOlmAccount::claimMultipleKeys() }); alice2->run(res2); - QVERIFY(spy.wait(10000)); QVERIFY(spy1.wait(10000)); QVERIFY(spy2.wait(1000)); // TODO this is failing even with 10000 @@ -502,7 +495,7 @@ void TestOlmAccount::keyChange() CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), QStringList()); - connect(job, &BaseJob::result, this, [alice, job, this] () { + connect(job, &BaseJob::result, this, [alice, job, this] { // Alice syncs to get the first next_batch token. alice->sync(); connect(alice.get(), &Connection::syncDone, this, [alice, this] { @@ -521,7 +514,7 @@ void TestOlmAccount::keyChange() // because of the key uploading. auto changeJob = alice->callApi(nextBatchToken, ""); - connect(changeJob, &BaseJob::result, this, [&changeJob, &alice] { + connect(changeJob, &BaseJob::result, this, [changeJob, alice] { QCOMPARE(changeJob->changed().size(), 1); QCOMPARE(changeJob->left().size(), 0); QCOMPARE(changeJob->changed()[0], alice->userId()); @@ -544,7 +537,7 @@ void TestOlmAccount::enableEncryption() QString joinedRoom; auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"}); - connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) { + connect(alice.get(), &Connection::newRoom, this, [alice, bob, joinedRoom, this] (Quotient::Room *room) { room->activateEncryption(); QSignalSpy spy(room, &Room::encryption); @@ -559,7 +552,7 @@ void TestOlmAccount::enableEncryption() QVERIFY(spy.wait(10000)); bob->sync(); - connect(bob.get(), &Connection::syncDone, this, [bob, &joinedRoom, this] { + connect(bob.get(), &Connection::syncDone, this, [bob, joinedRoom, this] { auto &events = bob->room(joinedRoom)->messageEvents(); bool hasEncryption = false; for (auto it = events.rbegin(); it != events.rend(); ++it) { -- cgit v1.2.3 From 2fc62d38e8f748d1a78baca8b6f2df40e7cfa1a9 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 30 Jan 2022 21:20:00 +0100 Subject: Use room() instead of provideRoom() --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 89b80909..dbc6261d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2068,7 +2068,7 @@ QString Connection::e2eeDataDir() const #ifdef Quotient_E2EE_ENABLED QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) { - auto room = provideRoom(notification["room_id"].toString()); + auto room = this->room(notification["room_id"].toString()); auto event = makeEvent(notification["event"].toObject()); auto decrypted = room->decryptMessage(*event); if(!decrypted) { -- cgit v1.2.3 From 023ef3005d3fae80637c6ce140e84db26250d564 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 30 Jan 2022 23:13:01 +0100 Subject: Port devices list to database --- lib/connection.cpp | 156 ++++++++++++++++++++++------------------------------- lib/database.cpp | 4 ++ 2 files changed, 67 insertions(+), 93 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index dbc6261d..54d79674 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -442,10 +442,9 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) completeSetup(loginJob->userId()); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; -#else // Quotient_E2EE_ENABLED +#endif // Quotient_E2EE_ENABLED database = new Database(loginJob->userId(), q); database->clear(); -#endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -1891,7 +1890,6 @@ QVector Connection::availableRoomVersions() co return result; } -#ifdef Quotient_E2EE_ENABLED void Connection::Private::loadOutdatedUserDevices() { QHash users; @@ -1930,112 +1928,84 @@ void Connection::Private::loadOutdatedUserDevices() }); } -void Connection::encryptionUpdate(Room *room) -{ - for(const auto &user : room->users()) { - if(!d->trackedUsers.contains(user->id())) { - d->trackedUsers += user->id(); - d->outdatedUsers += user->id(); - d->encryptionUpdateRequired = true; - } - } -} - void Connection::Private::saveDevicesList() { - if (!cacheState) - return; - - QElapsedTimer et; - et.start(); + q->database()->transaction(); + auto query = q->database()->prepareQuery(QStringLiteral("DELETE FROM tracked_users")); + q->database()->execute(query); + query.prepare(QStringLiteral("INSERT INTO tracked_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : trackedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } - QFile outFile { q->e2eeDataDir() % "/deviceslist.json" }; - if (!outFile.open(QFile::WriteOnly)) { - qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" - << outFile.errorString(); - qCWarning(E2EE) << "Caching the rooms state disabled"; - cacheState = false; - return; + query.prepare(QStringLiteral("DELETE FROM outdated_users")); + q->database()->execute(query); + query.prepare(QStringLiteral("INSERT INTO outdated_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : outdatedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); } - QJsonObject rootObj { - { QStringLiteral("cache_version"), - QJsonObject { - { QStringLiteral("major"), SyncData::cacheVersion().first }, - { QStringLiteral("minor"), SyncData::cacheVersion().second } } } - }; - { - QJsonArray trackedUsersJson; - QJsonArray outdatedUsersJson; - for (const auto &user : trackedUsers) { - trackedUsersJson += user; - } - for (const auto &user : outdatedUsers) { - outdatedUsersJson += user; + query.prepare(QStringLiteral("INSERT INTO tracked_devices(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);")); + for (const auto& user : deviceKeys.keys()) { + for (const auto& device : deviceKeys[user]) { + auto keys = device.keys.keys(); + auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1]; + auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1]; + + query.bindValue(":matrixId", user); + query.bindValue(":deviceId", device.deviceId); + query.bindValue(":curveKeyId", curveKeyId); + query.bindValue(":curveKey", device.keys[curveKeyId]); + query.bindValue(":edKeyId", edKeyId); + query.bindValue(":edKey", device.keys[edKeyId]); + + q->database()->execute(query); } - rootObj.insert(QStringLiteral("tracked_users"), trackedUsersJson); - rootObj.insert(QStringLiteral("outdated_users"), outdatedUsersJson); - const auto devicesList = toJson(deviceKeys); - rootObj.insert(QStringLiteral("devices_list"), devicesList); - rootObj.insert(QStringLiteral("sync_token"), q->nextBatchToken()); } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - const auto data = - cacheToBinary - ? QCborValue::fromJsonValue(rootObj).toCbor() - : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { rootObj }; - const auto data = cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif - qCDebug(PROFILER) << "DeviceList generated in" << et; - - outFile.write(data.data(), data.size()); - qCDebug(E2EE) << "DevicesList saved to" << outFile.fileName(); + q->database()->commit(); } void Connection::Private::loadDevicesList() { - QFile file { q->e2eeDataDir() % "/deviceslist.json" }; - if(!file.exists() || !file.open(QIODevice::ReadOnly)) { - qCDebug(E2EE) << "No devicesList cache exists. Creating new"; - 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) << "DevicesList cache is broken or empty, discarding"; - return; + auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;")); + q->database()->execute(query); + while(query.next()) { + trackedUsers += query.value(0).toString(); } - for(const auto &user : json["tracked_users"].toArray()) { - trackedUsers += user.toString(); + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;")); + q->database()->execute(query); + while(query.next()) { + outdatedUsers += query.value(0).toString(); } - for(const auto &user : json["outdated_users"].toArray()) { - outdatedUsers += user.toString(); + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;")); + q->database()->execute(query); + while(query.next()) { + deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys { + query.value("matrixId").toString(), + query.value("deviceId").toString(), + { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}, + {{query.value("curveKeyId").toString(), query.value("curveKey").toString()}, + {query.value("edKeyId").toString(), query.value("edKey").toString()}}, + {} // Signatures are not saved/loaded as they are not needed after initial validation + }; } - fromJson(json["devices_list"], deviceKeys); - auto oldToken = json["sync_token"].toString(); - auto changesJob = q->callApi(oldToken, q->nextBatchToken()); - connect(changesJob, &BaseJob::success, q, [this, changesJob](){ - bool hasNewOutdatedUser = false; - for(const auto &user : changesJob->changed()) { - outdatedUsers += user; - hasNewOutdatedUser = true; - } - if(hasNewOutdatedUser) { - loadOutdatedUserDevices(); +} + +#ifdef Quotient_E2EE_ENABLED +void Connection::encryptionUpdate(Room *room) +{ + for(const auto &user : room->users()) { + if(!d->trackedUsers.contains(user->id())) { + d->trackedUsers += user->id(); + d->outdatedUsers += user->id(); + d->encryptionUpdateRequired = true; } - }); + } } PicklingMode Connection::picklingMode() const diff --git a/lib/database.cpp b/lib/database.cpp index a5df22af..535920e2 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -86,6 +86,10 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); } -- cgit v1.2.3 From 27baabc9b8c9476fc550aef4462c193d5a9997a6 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 30 Jan 2022 23:15:42 +0100 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- autotests/testolmutility.cpp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index bbf3a055..d0476af0 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -47,14 +47,14 @@ void TestOlmUtility::canonicalJSON() void TestOlmUtility::verifySignedOneTimeKey() { - auto aliceOlm = std::make_shared("alice:matrix.org", "aliceDevice"); - aliceOlm->createNewAccount(); - aliceOlm->generateOneTimeKeys(1); - auto keys = aliceOlm->oneTimeKeys(); + QOlmAccount aliceOlm { "@alice:matrix.org", "aliceDevice" }; + aliceOlm.createNewAccount(); + aliceOlm.generateOneTimeKeys(1); + auto keys = aliceOlm.oneTimeKeys(); auto firstKey = keys.curve25519().keyValueBegin()->second; auto msgObj = QJsonObject({{"key", firstKey}}); - auto sig = aliceOlm->sign(msgObj); + auto sig = aliceOlm.sign(msgObj); auto msg = QJsonDocument(msgObj).toJson(QJsonDocument::Compact); @@ -66,8 +66,8 @@ void TestOlmUtility::verifySignedOneTimeKey() std::copy(sig.begin(), sig.end(), signatureBuf1.begin()); auto res = olm_ed25519_verify(utility, - aliceOlm->identityKeys().ed25519.data(), - aliceOlm->identityKeys().ed25519.size(), + aliceOlm.identityKeys().ed25519.data(), + aliceOlm.identityKeys().ed25519.size(), msg.data(), msg.size(), (void *)sig.data(), @@ -79,7 +79,7 @@ void TestOlmUtility::verifySignedOneTimeKey() delete[](reinterpret_cast(utility)); QOlmUtility utility2; - auto res2 = std::get(utility2.ed25519Verify(aliceOlm->identityKeys().ed25519, msg, signatureBuf1)); + auto res2 = std::get(utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg, signatureBuf1)); //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); QCOMPARE(res2, true); @@ -90,11 +90,11 @@ void TestOlmUtility::validUploadKeysRequest() const auto userId = QStringLiteral("@alice:matrix.org"); const auto deviceId = QStringLiteral("FKALSOCCC"); - auto alice = std::make_shared(userId, deviceId); - alice->createNewAccount(); - alice->generateOneTimeKeys(1); + QOlmAccount alice { userId, deviceId }; + alice.createNewAccount(); + alice.generateOneTimeKeys(1); - auto idSig = alice->signIdentityKeys(); + auto idSig = alice.signIdentityKeys(); QJsonObject body { @@ -103,8 +103,8 @@ void TestOlmUtility::validUploadKeysRequest() {"device_id", deviceId}, {"keys", QJsonObject{ - {QStringLiteral("curve25519:") + deviceId, QString::fromUtf8(alice->identityKeys().curve25519)}, - {QStringLiteral("ed25519:") + deviceId, QString::fromUtf8(alice->identityKeys().ed25519)} + {QStringLiteral("curve25519:") + deviceId, QString::fromUtf8(alice.identityKeys().curve25519)}, + {QStringLiteral("ed25519:") + deviceId, QString::fromUtf8(alice.identityKeys().ed25519)} } }, {"signatures", @@ -118,7 +118,7 @@ void TestOlmUtility::validUploadKeysRequest() } }; - DeviceKeys deviceKeys = alice->deviceKeys(); + DeviceKeys deviceKeys = alice.deviceKeys(); QCOMPARE(QJsonDocument(toJson(deviceKeys)).toJson(QJsonDocument::Compact), QJsonDocument(body).toJson(QJsonDocument::Compact)); -- cgit v1.2.3 From ac53741920b1e92b8ac61bb7c11afcae5722b241 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 6 Feb 2022 20:00:38 +0100 Subject: Update lib/e2ee/qolminboundsession.cpp Co-authored-by: Alexey Rusakov --- lib/e2ee/qolminboundsession.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 9729c02d..9bc80eef 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -4,7 +4,6 @@ #include "e2ee/qolminboundsession.h" #include -#include using namespace Quotient; QOlmError lastError(OlmInboundGroupSession *session) { -- cgit v1.2.3 From 9960d33e0c2bbe4dd8f305cae3b01c2d704d28ff Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 6 Feb 2022 20:00:01 +0100 Subject: Port to QRandomGenerator --- lib/e2ee/qolmutils.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp index ce27710d..6f7937e8 100644 --- a/lib/e2ee/qolmutils.cpp +++ b/lib/e2ee/qolmutils.cpp @@ -3,8 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "e2ee/qolmutils.h" -#include -#include +#include using namespace Quotient; @@ -19,6 +18,6 @@ QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) QByteArray Quotient::getRandom(size_t bufferSize) { QByteArray buffer(bufferSize, '0'); - RAND_bytes(reinterpret_cast(buffer.data()), buffer.size()); + QRandomGenerator::system()->generate(buffer.begin(), buffer.end()); return buffer; } -- cgit v1.2.3 From ef64359505778913235666f0e759dd0758f7f4ac Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 6 Feb 2022 20:06:19 +0100 Subject: Only create one database --- lib/connection.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 54d79674..58e3a9f8 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -443,8 +443,6 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #endif // Quotient_E2EE_ENABLED - database = new Database(loginJob->userId(), q); - database->clear(); }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -499,9 +497,7 @@ void Connection::Private::completeSetup(const QString& mxId) olmAccount = std::make_unique(data->userId(), data->deviceId(), q); connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); - if (!database) { - database = new Database(data->userId(), q); - } + database = new Database(data->userId(), q); encryptionManager = new EncryptionManager(q); -- cgit v1.2.3 From 4837fd6de188b849a5d11de6791ba844398ce415 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 6 Feb 2022 20:10:40 +0100 Subject: Update lib/e2ee/qolmaccount.cpp Co-authored-by: Alexey Rusakov --- lib/e2ee/qolmaccount.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index a984f884..34ee7ea0 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -29,7 +29,7 @@ std::optional> OneTimeKeys::get(QString keyType) const bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) { - return lhs.curve25519 == rhs.curve25519 &&& lhs.ed25519 == rhs.ed25519; + return lhs.curve25519 == rhs.curve25519 && lhs.ed25519 == rhs.ed25519; } // Convert olm error to enum -- cgit v1.2.3 From 7b5edb737522b03d4f697e0e09f1771ad5edef89 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 7 Feb 2022 21:48:07 +0100 Subject: Remove encryptionmanager and various fixes --- CMakeLists.txt | 2 +- autotests/testolmaccount.cpp | 2 +- lib/connection.cpp | 108 ++++++++++++++++++++++++----- lib/connection.h | 2 - lib/e2ee/qolminboundsession.cpp | 2 + lib/encryptionmanager.cpp | 149 ---------------------------------------- lib/encryptionmanager.h | 33 --------- lib/events/encryptedfile.cpp | 27 ++++++++ lib/events/encryptedfile.h | 6 ++ lib/jobs/downloadfilejob.cpp | 5 +- lib/mxcreply.cpp | 3 +- 11 files changed, 130 insertions(+), 209 deletions(-) delete mode 100644 lib/encryptionmanager.cpp delete mode 100644 lib/encryptionmanager.h create mode 100644 lib/events/encryptedfile.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef3477e..69ac7e20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,6 +156,7 @@ list(APPEND lib_SRCS lib/events/roomkeyevent.cpp lib/events/stickerevent.cpp lib/events/keyverificationevent.cpp + lib/events/encryptedfile.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp lib/jobs/syncjob.cpp @@ -174,7 +175,6 @@ if (${PROJECT_NAME}_ENABLE_E2EE) lib/e2ee/qolmerrors.cpp lib/e2ee/qolmsession.cpp lib/e2ee/qolmmessage.cpp - lib/encryptionmanager.cpp ) endif() diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 45d158eb..62b786d0 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -537,7 +537,7 @@ void TestOlmAccount::enableEncryption() QString joinedRoom; auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"}); - connect(alice.get(), &Connection::newRoom, this, [alice, bob, joinedRoom, this] (Quotient::Room *room) { + connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) { room->activateEncryption(); QSignalSpy spy(room, &Room::encryption); diff --git a/lib/connection.cpp b/lib/connection.cpp index 58e3a9f8..1a1b284d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -37,7 +37,6 @@ #ifdef Quotient_E2EE_ENABLED # include "e2ee/qolmaccount.h" # include "e2ee/qolmutils.h" -# include "encryptionmanager.h" # include "database.h" #if QT_VERSION_MAJOR >= 6 @@ -117,6 +116,10 @@ public: bool encryptionUpdateRequired = false; PicklingMode picklingMode = Unencrypted {}; Database *database = nullptr; + + // A map from SenderKey to vector of InboundSession + UnorderedMap> olmSessions; + #endif GetCapabilitiesJob* capabilitiesJob = nullptr; @@ -127,7 +130,6 @@ public: #ifdef Quotient_E2EE_ENABLED std::unique_ptr olmAccount; bool isUploadingKeys = false; - EncryptionManager *encryptionManager; #endif // Quotient_E2EE_ENABLED QPointer resolverJob = nullptr; @@ -201,6 +203,85 @@ public: return q->stateCacheDir().filePath("state.json"); } +#ifdef Quotient_E2EE_ENABLED + void loadSessions() { + olmSessions = q->database()->loadOlmSessions(q->picklingMode()); + } + void saveSession(QOlmSessionPtr& session, const QString &senderKey) { + auto pickleResult = session->pickle(q->picklingMode()); + if (std::holds_alternative(pickleResult)) { + qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get(pickleResult); + return; + } + q->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult)); + } + QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) + { + Q_ASSERT(message.type() == QOlmMessage::PreKey); + for(auto& session : olmSessions[senderKey]) { + const auto matches = session->matchesInboundSessionFrom(senderKey, message); + if(std::holds_alternative(matches) && std::get(matches)) { + qCDebug(E2EE) << "Found inbound session"; + const auto result = session->decrypt(message); + if(std::holds_alternative(result)) { + return std::get(result); + } else { + qCDebug(E2EE) << "Failed to decrypt prekey message"; + return {}; + } + } + } + qCDebug(E2EE) << "Creating new inbound session"; + auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); + if(std::holds_alternative(newSessionResult)) { + qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get(newSessionResult); + return {}; + } + auto newSession = std::move(std::get(newSessionResult)); + auto error = olmAccount->removeOneTimeKeys(newSession); + if (error) { + qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); + } + const auto result = newSession->decrypt(message); + saveSession(newSession, senderKey); + olmSessions[senderKey].push_back(std::move(newSession)); + if(std::holds_alternative(result)) { + return std::get(result); + } else { + qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; + return {}; + } + } + QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) + { + Q_ASSERT(message.type() == QOlmMessage::General); + for(auto& session : olmSessions[senderKey]) { + const auto result = session->decrypt(message); + if(std::holds_alternative(result)) { + return std::get(result); + } + } + qCWarning(E2EE) << "Failed to decrypt message"; + return {}; + } + + QString sessionDecryptMessage( + const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account) + { + QString decrypted; + int type = personalCipherObject.value(TypeKeyL).toInt(-1); + QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); + if (type == 0) { + QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); + decrypted = sessionDecryptPrekey(preKeyMessage, senderKey, account); + } else if (type == 1) { + QOlmMessage message(body, QOlmMessage::General); + decrypted = sessionDecryptGeneral(message, senderKey); + } + return decrypted; + } +#endif + EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED @@ -217,7 +298,7 @@ public: qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } - const auto decrypted = encryptionManager->sessionDecryptMessage( + const auto decrypted = sessionDecryptMessage( personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" @@ -443,6 +524,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #endif // Quotient_E2EE_ENABLED + database->clear(); }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -493,13 +575,15 @@ void Connection::Private::completeSetup(const QString& mxId) picklingMode = Encrypted { job.binaryData() }; } + database = new Database(data->userId(), q); + // init olmAccount olmAccount = std::make_unique(data->userId(), data->deviceId(), q); connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); - database = new Database(data->userId(), q); - - encryptionManager = new EncryptionManager(q); +#ifdef Quotient_E2EE_ENABLED + loadSessions(); +#endif if (database->accountPickle().isEmpty()) { // create new account and save unpickle data @@ -2019,18 +2103,6 @@ void Connection::saveOlmAccount() #endif } -QString Connection::e2eeDataDir() const -{ - auto safeUserId = userId(); - safeUserId.replace(':', '_'); - const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) % '/' - % safeUserId % '/'; - QDir dir; - if (!dir.exists(path)) - dir.mkpath(path); - return path; -} - #ifdef Quotient_E2EE_ENABLED QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) { diff --git a/lib/connection.h b/lib/connection.h index 93ee496e..8dec2a0c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -401,8 +401,6 @@ public: bool lazyLoading() const; void setLazyLoading(bool newValue); - QString e2eeDataDir() const; - /*! Start a pre-created job object on this connection */ Q_INVOKABLE BaseJob* run(BaseJob* job, RunningPolicy runningPolicy = ForegroundRequest); diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 9bc80eef..2e9cc716 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -4,6 +4,8 @@ #include "e2ee/qolminboundsession.h" #include +#include + using namespace Quotient; QOlmError lastError(OlmInboundGroupSession *session) { diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp deleted file mode 100644 index abdcdcee..00000000 --- a/lib/encryptionmanager.cpp +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev -// SPDX-FileCopyrightText: 2019 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#include "encryptionmanager.h" - -#include "connection.h" -#include "events/encryptedfile.h" -#include "database.h" - -#include "csapi/keys.h" - -#include -#include -#include - -#include "e2ee/e2ee.h" -#include "e2ee/qolmaccount.h" -#include "e2ee/qolmsession.h" -#include "e2ee/qolmmessage.h" -#include "e2ee/qolmerrors.h" -#include "e2ee/qolmutils.h" -#include -#include - -#include - -using namespace Quotient; -using std::move; - -class EncryptionManager::Private { -public: - EncryptionManager* q; - - Connection* connection; - - // A map from SenderKey to vector of InboundSession - UnorderedMap> sessions; - - void loadSessions() { - sessions = connection->database()->loadOlmSessions(connection->picklingMode()); - } - void saveSession(QOlmSessionPtr& session, const QString &senderKey) { - auto pickleResult = session->pickle(connection->picklingMode()); - if (std::holds_alternative(pickleResult)) { - qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get(pickleResult); - return; - } - connection->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult)); - } - QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) - { - Q_ASSERT(message.type() == QOlmMessage::PreKey); - for(auto& session : sessions[senderKey]) { - const auto matches = session->matchesInboundSessionFrom(senderKey, message); - if(std::holds_alternative(matches) && std::get(matches)) { - qCDebug(E2EE) << "Found inbound session"; - const auto result = session->decrypt(message); - if(std::holds_alternative(result)) { - return std::get(result); - } else { - qCDebug(E2EE) << "Failed to decrypt prekey message"; - return {}; - } - } - } - qCDebug(E2EE) << "Creating new inbound session"; - auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); - if(std::holds_alternative(newSessionResult)) { - qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get(newSessionResult); - return {}; - } - auto newSession = std::move(std::get(newSessionResult)); - auto error = olmAccount->removeOneTimeKeys(newSession); - if (error) { - qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); - } - const auto result = newSession->decrypt(message); - saveSession(newSession, senderKey); - sessions[senderKey].push_back(std::move(newSession)); - if(std::holds_alternative(result)) { - return std::get(result); - } else { - qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; - return {}; - } - } - QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) - { - Q_ASSERT(message.type() == QOlmMessage::General); - for(auto& session : sessions[senderKey]) { - const auto result = session->decrypt(message); - if(std::holds_alternative(result)) { - return std::get(result); - } - } - qCWarning(E2EE) << "Failed to decrypt message"; - return {}; - } -}; - -EncryptionManager::EncryptionManager(QObject* parent) - : QObject(parent) - , d(std::make_unique()) -{ - d->q = this; - d->connection = static_cast(parent); - d->loadSessions(); -} - -EncryptionManager::~EncryptionManager() = default; - -QString EncryptionManager::sessionDecryptMessage( - const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account) -{ - QString decrypted; - int type = personalCipherObject.value(TypeKeyL).toInt(-1); - QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); - if (type == 0) { - QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); - decrypted = d->sessionDecryptPrekey(preKeyMessage, senderKey, account); - } else if (type == 1) { - QOlmMessage message(body, QOlmMessage::General); - decrypted = d->sessionDecryptGeneral(message, senderKey); - } - return decrypted; -} - -QByteArray EncryptionManager::decryptFile(const QByteArray &ciphertext, EncryptedFile* file) -{ - const auto key = QByteArray::fromBase64(file->key.k.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); - const auto iv = QByteArray::fromBase64(file->iv.toLatin1()); - const auto sha256 = QByteArray::fromBase64(file->hashes["sha256"].toLatin1()); - if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { - qCWarning(E2EE) << "Hash verification failed for file"; - return QByteArray(); - } - QByteArray plaintext(ciphertext.size(), 0); - EVP_CIPHER_CTX *ctx; - int length; - ctx = EVP_CIPHER_CTX_new(); - EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)key.data(), (const unsigned char *)iv.data()); - EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); - EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext; -} -#endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h deleted file mode 100644 index 96569980..00000000 --- a/lib/encryptionmanager.h +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifdef Quotient_E2EE_ENABLED -#pragma once - -#include - -#include -#include - -namespace Quotient { -class Connection; -class QOlmAccount; -struct EncryptedFile; - -class EncryptionManager : public QObject { - Q_OBJECT - -public: - explicit EncryptionManager(QObject* parent = nullptr); - ~EncryptionManager(); - QString sessionDecryptMessage(const QJsonObject& personalCipherObject, - const QByteArray& senderKey, std::unique_ptr& account); - static QByteArray decryptFile(const QByteArray &ciphertext, EncryptedFile* encryptedFile); - -private: - class Private; - std::unique_ptr d; -}; - -} // namespace Quotient -#endif // Quotient_E2EE_ENABLED diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp new file mode 100644 index 00000000..5ec344bb --- /dev/null +++ b/lib/events/encryptedfile.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "encryptedfile.h" + +using namespace Quotient; + +QByteArray EncryptedFile::decryptFile(const QByteArray &ciphertext) const +{ + QString _key = key.k; + _key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); + const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); + if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return QByteArray(); + } + QByteArray plaintext(ciphertext.size(), 0); + EVP_CIPHER_CTX *ctx; + int length; + ctx = EVP_CIPHER_CTX_new(); + EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)_key.data(), (const unsigned char *)iv.toLatin1().data()); + EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); + EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext; +} diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 24ac9de1..f271d345 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -5,6 +5,10 @@ #pragma once #include "converters.h" +#include "logging.h" + +#include +#include namespace Quotient { /** @@ -44,6 +48,8 @@ public: QString iv; QHash hashes; QString v; + + QByteArray decryptFile(const QByteArray &ciphertext) const; }; template <> diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 2eea9d59..c5280770 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -9,7 +9,6 @@ #ifdef Quotient_E2EE_ENABLED # include -# include "encryptionmanager.h" # include "events/encryptedfile.h" #endif @@ -126,7 +125,7 @@ BaseJob::Status DownloadFileJob::prepareResult() QByteArray encrypted = d->tempFile->readAll(); EncryptedFile file = *d->encryptedFile; - auto decrypted = EncryptionManager::decryptFile(encrypted, &file); + auto decrypted = file.decryptFile(encrypted); d->targetFile->write(decrypted); d->tempFile->remove(); } else { @@ -151,7 +150,7 @@ BaseJob::Status DownloadFileJob::prepareResult() auto encrypted = d->tempFile->readAll(); EncryptedFile file = *d->encryptedFile; - auto decrypted = EncryptionManager::decryptFile(encrypted, &file); + auto decrypted = file.decryptFile(encrypted); d->tempFile->write(decrypted); } else { #endif diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index c7f27b0c..c666cce3 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -9,7 +9,6 @@ #include "room.h" #ifdef Quotient_E2EE_ENABLED -#include "encryptionmanager.h" #include "events/encryptedfile.h" #endif @@ -51,7 +50,7 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) } else { EncryptedFile file = *d->m_encryptedFile; auto buffer = new QBuffer(this); - buffer->setData(EncryptionManager::decryptFile(d->m_reply->readAll(), &file)); + buffer->setData(file.decryptFile(d->m_reply->readAll())); buffer->open(ReadOnly); d->m_device = buffer; } -- cgit v1.2.3 From 840aead6e77a7ab8605bd2f70820ddd2219bdad5 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 7 Feb 2022 22:04:38 +0100 Subject: Refactor KeyVerificationEvents --- lib/events/keyverificationevent.cpp | 83 ++++++++++++------------------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp index 938b3bde..4803955d 100644 --- a/lib/events/keyverificationevent.cpp +++ b/lib/events/keyverificationevent.cpp @@ -11,26 +11,22 @@ KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj) QString KeyVerificationRequestEvent::fromDevice() const { - return contentJson()["from_device"_ls].toString(); + return contentPart("from_device"_ls); } QString KeyVerificationRequestEvent::transactionId() const { - return contentJson()["transaction_id"_ls].toString(); + return contentPart("transaction_id"_ls); } QStringList KeyVerificationRequestEvent::methods() const { - QStringList methods; - for (const auto &method : contentJson()["methods"].toArray()) { - methods.append(method.toString()); - } - return methods; + return contentPart("methods"_ls); } uint64_t KeyVerificationRequestEvent::timestamp() const { - return contentJson()["timestamp"_ls].toDouble(); + return contentPart("timestamp"_ls); } KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj) @@ -39,62 +35,46 @@ KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj) QString KeyVerificationStartEvent::fromDevice() const { - return contentJson()["from_device"_ls].toString(); + return contentPart("from_device"_ls); } QString KeyVerificationStartEvent::transactionId() const { - return contentJson()["transaction_id"_ls].toString(); + return contentPart("transaction_id"_ls); } QString KeyVerificationStartEvent::method() const { - return contentJson()["method"_ls].toString(); + return contentPart("method"_ls); } Omittable KeyVerificationStartEvent::nextMethod() const { - auto next = contentJson()["method"_ls]; - if (next.isUndefined()) { - return std::nullopt; - } - return next.toString(); + return contentPart>("method_ls"); } QStringList KeyVerificationStartEvent::keyAgreementProtocols() const { Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - QStringList protocols; - for (const auto &proto : contentJson()["key_agreement_protocols"_ls].toArray()) { - protocols.append(proto.toString()); - } - return protocols; + return contentPart("key_agreement_protocols"_ls); } QStringList KeyVerificationStartEvent::hashes() const { Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - QStringList hashes; - for (const auto &hashItem : contentJson()["hashes"_ls].toArray()) { - hashes.append(hashItem.toString()); - } - return hashes; + return contentPart("hashes"_ls); + } QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const { Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - - QStringList codes; - for (const auto &code : contentJson()["message_authentication_codes"_ls].toArray()) { - codes.append(code.toString()); - } - return codes; + return contentPart("message_authentication_codes"_ls); } QString KeyVerificationStartEvent::shortAuthenticationString() const { - return contentJson()["short_authentification_string"_ls].toString(); + return contentPart("short_authentification_string"_ls); } KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj) @@ -103,36 +83,32 @@ KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj) QString KeyVerificationAcceptEvent::transactionId() const { - return contentJson()["transaction_id"_ls].toString(); + return contentPart("transaction_id"_ls); } QString KeyVerificationAcceptEvent::method() const { - return contentJson()["method"_ls].toString(); + return contentPart("method"_ls); } QString KeyVerificationAcceptEvent::keyAgreementProtocol() const { - return contentJson()["key_agreement_protocol"_ls].toString(); + return contentPart("key_agreement_protocol"_ls); } QString KeyVerificationAcceptEvent::hashData() const { - return contentJson()["hash"_ls].toString(); + return contentPart("hash"_ls); } QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const { - QStringList strings; - for (const auto &authenticationString : contentJson()["short_authentification_string"].toArray()) { - strings.append(authenticationString.toString()); - } - return strings; + return contentPart("short_authentification_string"_ls); } QString KeyVerificationAcceptEvent::commitement() const { - return contentJson()["commitement"].toString(); + return contentPart("commitment"_ls); } KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj) @@ -141,17 +117,17 @@ KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj) QString KeyVerificationCancelEvent::transactionId() const { - return contentJson()["transaction_id"_ls].toString(); + return contentPart("transaction_id"_ls); } QString KeyVerificationCancelEvent::reason() const { - return contentJson()["reason"_ls].toString(); + return contentPart("reason"_ls); } QString KeyVerificationCancelEvent::code() const { - return contentJson()["code"_ls].toString(); + return contentPart("code"_ls); } KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj) @@ -160,12 +136,12 @@ KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj) QString KeyVerificationKeyEvent::transactionId() const { - return contentJson()["transaction_id"_ls].toString(); + return contentPart("transaction_id"_ls); } QString KeyVerificationKeyEvent::key() const { - return contentJson()["key"_ls].toString(); + return contentPart("key"_ls); } KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj) @@ -174,20 +150,15 @@ KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj) QString KeyVerificationMacEvent::transactionId() const { - return contentJson()["transaction_id"].toString(); + return contentPart("transaction_id"_ls); } QString KeyVerificationMacEvent::keys() const { - return contentJson()["keys"].toString(); + return contentPart("keys"_ls); } QHash KeyVerificationMacEvent::mac() const { - QHash macs; - const auto macObj = contentJson()["mac"_ls].toObject(); - for (auto mac = macObj.constBegin(); mac != macObj.constEnd(); mac++) { - macs.insert(mac.key(), mac.value().toString()); - } - return macs; + return contentPart>("mac"_ls); } -- cgit v1.2.3 From 94f34099b8a4c8a40cc99496ceaf9ad5b285c08f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 9 Feb 2022 22:18:42 +0100 Subject: Move includes to .cpp file --- lib/events/encryptedfile.cpp | 4 ++++ lib/events/encryptedfile.h | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index 5ec344bb..74119127 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -3,6 +3,10 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedfile.h" +#include "logging.h" + +#include +#include using namespace Quotient; diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index f271d345..6199be8e 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -5,10 +5,6 @@ #pragma once #include "converters.h" -#include "logging.h" - -#include -#include namespace Quotient { /** -- cgit v1.2.3 From 6415d6fb194799870eb89cbaff4ba07939aa6ccb Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 9 Feb 2022 22:25:24 +0100 Subject: Fix compilation without E2EE --- lib/connection.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 1a1b284d..04ce1dc2 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -523,8 +523,9 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) completeSetup(loginJob->userId()); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; -#endif // Quotient_E2EE_ENABLED +#else // Quotient_E2EE_ENABLED database->clear(); +#endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -1970,6 +1971,7 @@ QVector Connection::availableRoomVersions() co return result; } +#ifdef Quotient_E2EE_ENABLED void Connection::Private::loadOutdatedUserDevices() { QHash users; @@ -2076,7 +2078,6 @@ void Connection::Private::loadDevicesList() } -#ifdef Quotient_E2EE_ENABLED void Connection::encryptionUpdate(Room *room) { for(const auto &user : room->users()) { -- cgit v1.2.3 From 73623b69477352f901aa0d02a667bd2438a91491 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 11 Feb 2022 14:22:57 +0100 Subject: EventRelation: defer to non-deprecated symbols --- lib/events/roommessageevent.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 44ef05fb..03a51328 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -101,8 +101,8 @@ namespace EventContent { struct [[deprecated("Use Quotient::EventRelation instead")]] RelatesTo : EventRelation { - static constexpr auto ReplyTypeId() { return Reply(); } - static constexpr auto ReplacementTypeId() { return Replacement(); } + static constexpr auto ReplyTypeId() { return ReplyType; } + static constexpr auto ReplacementTypeId() { return ReplacementType; } }; [[deprecated("Use EventRelation::replyTo() instead")]] inline auto replyTo(QString eventId) -- cgit v1.2.3 From 4f581f22d707fd226a56400817c9e47e54116589 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 11 Feb 2022 14:21:25 +0100 Subject: Better documentation for QUO_DECLARE_FLAGS[_NS] --- lib/quotient_common.h | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index b3fb3efa..2b785a39 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -9,17 +9,33 @@ #include -// See https://bugreports.qt.io/browse/QTBUG-82295 - despite the comment that -// Q_FLAG[_NS] "should" be applied to the enum only, Qt doesn't allow to wrap -// a flag type into a QVariant then. The macros below define Q_FLAG[_NS] and on -// top of that add Q_ENUM[_NS]_IMPL which is a part of Q_ENUM() macro that -// enables the metatype data but goes under the moc radar to avoid double -// registration of the same data in the map defined in moc_*.cpp + +//! \brief Quotient replacement for the Q_FLAG/Q_DECLARE_FLAGS combination +//! +//! Although the comment in QTBUG-82295 says that Q_FLAG[_NS] "should" be +//! applied to the enum type only, Qt then doesn't allow to wrap the +//! corresponding flag type (defined with Q_DECLARE_FLAGS) into a QVariant. +//! This macro defines Q_FLAG and on top of that adds Q_ENUM_IMPL which is +//! a part of Q_ENUM() macro that enables the metatype data but goes under +//! the moc radar to avoid double registration of the same data in the map +//! defined in moc_*.cpp. +//! +//! Simply put, instead of using Q_FLAG/Q_DECLARE_FLAGS combo (and struggling +//! to figure out what you should pass to Q_FLAG if you want to make it +//! wrappable in a QVariant) use the macro below, and things will just work. +//! +//! \sa https://bugreports.qt.io/browse/QTBUG-82295 #define QUO_DECLARE_FLAGS(Flags, Enum) \ Q_DECLARE_FLAGS(Flags, Enum) \ Q_ENUM_IMPL(Enum) \ Q_FLAG(Flags) +//! \brief Quotient replacement for the Q_FLAG_NS/Q_DECLARE_FLAGS combination +//! +//! This is the equivalent of QUO_DECLARE_FLAGS for enums declared at the +//! namespace level (be sure to provide Q_NAMESPACE _in the same file_ +//! as the enum definition and this macro). +//! \sa QUO_DECLARE_FLAGS #define QUO_DECLARE_FLAGS_NS(Flags, Enum) \ Q_DECLARE_FLAGS(Flags, Enum) \ Q_ENUM_NS_IMPL(Enum) \ -- cgit v1.2.3 From 10ac7c13cdcd62b62af6e89cb726376cfbc53302 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 3 Jan 2022 21:59:04 +0100 Subject: Enable to/fromJson to work with non-assignable objects Previously you could not use toJson() on a polymorphic structure such as one of those defined in eventcontent.h because it is not assignable and the default specialisation of JsonObjectConverter used assignment. To avoid that limitation, one had to specialise JsonObjectConverter for each descendant of EventContent::Base, which is a lot of boilerplate. The new JsonConverter (the template underpinning the "default" to/fromJson implementation) improves on two things: 1. dump() allows to construct your own QJsonObject - or anything else convertable to QJsonValue - instead of requiring to fill in the pre-constructed one. 2. load() allows to construct your value type directly from QJsonObject instead of default-constructing it in advance. --- lib/converters.h | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index 8ddf6e45..515c96fd 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -14,6 +14,7 @@ #include #include +#include #include class QVariant; @@ -21,10 +22,29 @@ class QVariant; namespace Quotient { template struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } - static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } + // To be implemented in specialisations + static void dumpTo(QJsonObject&, const T&) = delete; + static void fillFrom(const QJsonObject&, T&) = delete; }; +namespace _impl { + template + struct JsonExporter { + static QJsonObject dump(const T& data) + { + QJsonObject jo; + JsonObjectConverter::dumpTo(jo, data); + return jo; + } + }; + + template + struct JsonExporter< + T, std::enable_if_t>> { + static auto dump(const T& data) { return data.toJson(); } + }; +} + //! \brief The switchboard for extra conversion algorithms behind from/toJson //! //! This template is mainly intended for partial conversion specialisations @@ -41,18 +61,25 @@ struct JsonObjectConverter { //! that they are not supported and it's not feasible to support those by means //! of overloading toJson() and specialising fromJson(). template -struct JsonConverter { - static QJsonObject dump(const T& pod) - { - QJsonObject jo; - JsonObjectConverter::dumpTo(jo, pod); - return jo; - } +struct JsonConverter : _impl::JsonExporter { + // Unfortunately, if constexpr doesn't work with dump() and T::toJson + // because trying to check invocability of T::toJson hits a hard + // (non-SFINAE) compilation error if the member is not there. Hence a bit + // more verbose SFINAE construct in _impl::JsonExporter. + static T doLoad(const QJsonObject& jo) { - T pod; - JsonObjectConverter::fillFrom(jo, pod); - return pod; + // 'else' below are required to suppress code generation for unused + // branches - 'return' is not enough + if constexpr (std::is_same_v) + return jo; + else if constexpr (std::is_constructible_v) + return T(jo); + else { + T pod; + JsonObjectConverter::fillFrom(jo, pod); + return pod; + } } static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } -- 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/connection.cpp | 11 +++++++++++ lib/connection.h | 3 +++ lib/room.cpp | 9 +++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 04ce1dc2..cc5d8739 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -38,6 +38,7 @@ # include "e2ee/qolmaccount.h" # include "e2ee/qolmutils.h" # include "database.h" +# include "e2ee/qolminboundsession.h" #if QT_VERSION_MAJOR >= 6 # include @@ -2120,4 +2121,14 @@ Database* Connection::database() { return d->database; } + +UnorderedMap, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room) +{ + return database()->loadMegolmSessions(room->id(), picklingMode()); +} + +void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session) +{ + database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), session->pickle(picklingMode())); +} #endif diff --git a/lib/connection.h b/lib/connection.h index 8dec2a0c..13aa15c0 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -50,6 +50,7 @@ class LeaveRoomJob; class Database; class QOlmAccount; +class QOlmInboundGroupSession; using LoginFlow = GetLoginFlowsJob::LoginFlow; @@ -315,6 +316,8 @@ public: #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; Database* database(); + UnorderedMap, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); + void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; 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/connection.cpp | 2 +- lib/connection.h | 2 +- lib/database.cpp | 6 +++--- lib/database.h | 4 ++-- lib/room.cpp | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index cc5d8739..a8de4030 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2122,7 +2122,7 @@ Database* Connection::database() return d->database; } -UnorderedMap, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room) +UnorderedMap, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room) { return database()->loadMegolmSessions(room->id(), picklingMode()); } diff --git a/lib/connection.h b/lib/connection.h index 13aa15c0..28ea6ff3 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -316,7 +316,7 @@ public: #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; Database* database(); - UnorderedMap, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); + UnorderedMap, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; diff --git a/lib/database.cpp b/lib/database.cpp index 535920e2..b91b6ef1 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -160,14 +160,14 @@ UnorderedMap> Database::loadOlmSessions(con return sessions; } -UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) +UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) { auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;")); query.bindValue(":roomId", roomId); transaction(); execute(query); commit(); - UnorderedMap, QOlmInboundGroupSessionPtr> sessions; + UnorderedMap, QOlmInboundGroupSessionPtr> sessions; while (query.next()) { auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); if (std::holds_alternative(session)) { @@ -204,7 +204,7 @@ void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& commit(); } -QPair Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) +std::pair Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) { auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); query.bindValue(":roomId", roomId); diff --git a/lib/database.h b/lib/database.h index b2187ba4..96256a55 100644 --- a/lib/database.h +++ b/lib/database.h @@ -30,10 +30,10 @@ public: void clear(); void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); - UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); + UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); - QPair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); + std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); private: 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 2cf44607cf0f057e147c2c4fe6dded6c13c58a8a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 14 Feb 2022 14:45:38 +0100 Subject: Don't #include "logging.h" from headers Logging categories used by Quotient are not supposed to be exposed externally, which basically forbids usage of logging in header files. A more flexible solution would involve moving logging.h to private headers but Quotient doesn't have that thing yet. --- lib/events/event.h | 1 - lib/syncdata.cpp | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/events/event.h b/lib/events/event.h index 113fa3fa..b6f36306 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -4,7 +4,6 @@ #pragma once #include "converters.h" -#include "logging.h" #include "function_traits.h" namespace Quotient { diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index b0cd8e4d..b896d710 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -3,6 +3,8 @@ #include "syncdata.h" +#include "logging.h" + #include "events/eventloader.h" #include -- cgit v1.2.3 From 7221c10118a8895ec39ba6a78768574a4b00dfed Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 14 Feb 2022 15:41:36 +0100 Subject: Revert "Don't #include "logging.h" from headers" This reverts commit 2cf44607cf0f057e147c2c4fe6dded6c13c58a8a (that was stupid, honestly). --- lib/events/event.h | 1 + lib/syncdata.cpp | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index b6f36306..113fa3fa 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -4,6 +4,7 @@ #pragma once #include "converters.h" +#include "logging.h" #include "function_traits.h" namespace Quotient { diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index b896d710..b0cd8e4d 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -3,8 +3,6 @@ #include "syncdata.h" -#include "logging.h" - #include "events/eventloader.h" #include -- cgit v1.2.3 From 34cd6a6c6dfe2981001341f39e4e1e9aaa9c8898 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 14 Feb 2022 21:46:27 +0100 Subject: Don't set lifetime as version in call invites --- lib/events/callinviteevent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 5ea54662..11d50768 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -36,7 +36,7 @@ CallInviteEvent::CallInviteEvent(const QJsonObject& obj) CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, const QString& sdp) : CallEventBase( - typeId(), matrixTypeId(), callId, lifetime, + typeId(), matrixTypeId(), callId, 0, { { QStringLiteral("lifetime"), lifetime }, { QStringLiteral("offer"), QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") }, -- cgit v1.2.3 From 52a787eefb3fb3d147648d08fc439a4b8a966fd3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 16 Feb 2022 17:57:30 +0100 Subject: Add a few missing QUOTIENT_API stanzas Also, removed Q_GADGET macros from key verification events as those don't seem to do anything (no Q_ENUM/Q_FLAG things, namely). --- lib/database.h | 3 +-- lib/e2ee/e2ee.h | 2 +- lib/e2ee/qolmaccount.h | 14 +++++++------- lib/e2ee/qolmerrors.h | 4 +++- lib/e2ee/qolminboundsession.h | 2 +- lib/e2ee/qolmmessage.h | 4 +++- lib/e2ee/qolmoutboundsession.h | 2 +- lib/e2ee/qolmsession.h | 2 +- lib/e2ee/qolmutility.h | 2 +- lib/e2ee/qolmutils.h | 4 ++-- lib/events/encryptedfile.h | 2 +- lib/events/keyverificationevent.h | 16 +++++----------- 12 files changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/database.h b/lib/database.h index 96256a55..d244dc0b 100644 --- a/lib/database.h +++ b/lib/database.h @@ -10,10 +10,9 @@ #include "e2ee/e2ee.h" namespace Quotient { -class Database : public QObject +class QUOTIENT_API Database : public QObject { Q_OBJECT - public: Database(const QString& matrixId, QObject* parent); diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 41cd2878..4c825376 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -65,7 +65,7 @@ struct IdentityKeys }; //! Struct representing the one-time keys. -struct OneTimeKeys +struct QUOTIENT_API OneTimeKeys { QMap> keys; diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 00afc0e6..17aca8aa 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -25,7 +25,7 @@ using QOlmSessionPtr = std::unique_ptr; //! \code{.cpp} //! const auto olmAccount = new QOlmAccount(this); //! \endcode -class QOlmAccount : public QObject +class QUOTIENT_API QOlmAccount : public QObject { Q_OBJECT public: @@ -111,13 +111,13 @@ private: QString m_deviceId; }; -bool verifyIdentitySignature(const DeviceKeys &deviceKeys, - const QString &deviceId, - const QString &userId); +QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId); //! checks if the signature is signed by the signing_key -bool ed25519VerifySignature(const QString &signingKey, - const QJsonObject &obj, - const QString &signature); +QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature); } // namespace Quotient diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h index 24e87d95..20e61c12 100644 --- a/lib/e2ee/qolmerrors.h +++ b/lib/e2ee/qolmerrors.h @@ -4,6 +4,8 @@ #pragma once +#include "quotient_export.h" + namespace Quotient { //! All errors that could be caused by an operation regarding Olm //! Errors are named exactly like the ones in libolm. @@ -21,6 +23,6 @@ enum QOlmError Unknown, }; -QOlmError fromString(const char* error_raw); +QUOTIENT_API QOlmError fromString(const char* error_raw); } //namespace Quotient diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 7d52991c..1f5dadd3 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -15,7 +15,7 @@ namespace Quotient { //! An in-bound group session is responsible for decrypting incoming //! communication in a Megolm session. -struct QOlmInboundGroupSession +class QUOTIENT_API QOlmInboundGroupSession { public: ~QOlmInboundGroupSession(); diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h index 52aba78c..557c02b1 100644 --- a/lib/e2ee/qolmmessage.h +++ b/lib/e2ee/qolmmessage.h @@ -4,6 +4,8 @@ #pragma once +#include "quotient_export.h" + #include #include @@ -16,7 +18,7 @@ namespace Quotient { * * The class provides functions to get a type and the ciphertext. */ -class QOlmMessage : public QByteArray { +class QUOTIENT_API QOlmMessage : public QByteArray { Q_GADGET public: enum Type { diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 39263c77..0122bbfd 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -13,7 +13,7 @@ namespace Quotient { //! An out-bound group session is responsible for encrypting outgoing //! communication in a Megolm session. -class QOlmOutboundGroupSession +class QUOTIENT_API QOlmOutboundGroupSession { public: ~QOlmOutboundGroupSession(); diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index 1febfa0f..889a606d 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -18,7 +18,7 @@ class QOlmSession; //! Either an outbound or inbound session for secure communication. -class QOlmSession +class QUOTIENT_API QOlmSession { public: ~QOlmSession(); diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index b360d625..b2e79e29 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -17,7 +17,7 @@ class Connection; //! Allows you to make use of crytographic hashing via SHA-2 and //! verifying ed25519 signatures. -class QOlmUtility +class QUOTIENT_API QOlmUtility { public: QOlmUtility(); diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h index bbd71332..f218e628 100644 --- a/lib/e2ee/qolmutils.h +++ b/lib/e2ee/qolmutils.h @@ -10,6 +10,6 @@ namespace Quotient { // Convert PicklingMode to key -QByteArray toKey(const PicklingMode &mode); -QByteArray getRandom(size_t bufferSize); +QUOTIENT_API QByteArray toKey(const PicklingMode &mode); +QUOTIENT_API QByteArray getRandom(size_t bufferSize); } diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 6199be8e..43bafc49 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -29,7 +29,7 @@ public: bool ext; }; -struct EncryptedFile +struct QUOTIENT_API EncryptedFile { Q_GADGET Q_PROPERTY(QUrl url MEMBER url CONSTANT) diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 13e7dcdd..497e56a2 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -7,8 +7,7 @@ namespace Quotient { /// Requests a key verification with another user's devices. /// Typically sent as a to-device event. -class KeyVerificationRequestEvent : public Event { - Q_GADGET +class QUOTIENT_API KeyVerificationRequestEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent) @@ -33,8 +32,7 @@ public: REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) /// Begins a key verification process. -class KeyVerificationStartEvent : public Event { - Q_GADGET +class QUOTIENT_API KeyVerificationStartEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) @@ -76,8 +74,7 @@ REGISTER_EVENT_TYPE(KeyVerificationStartEvent) /// Accepts a previously sent m.key.verification.start message. /// Typically sent as a to-device event. -class KeyVerificationAcceptEvent : public Event { - Q_GADGET +class QUOTIENT_API KeyVerificationAcceptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent) @@ -111,8 +108,7 @@ public: }; REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) -class KeyVerificationCancelEvent : public Event { - Q_GADGET +class QUOTIENT_API KeyVerificationCancelEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) @@ -133,7 +129,6 @@ REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. class KeyVerificationKeyEvent : public Event { - Q_GADGET public: DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) @@ -148,8 +143,7 @@ public: REGISTER_EVENT_TYPE(KeyVerificationKeyEvent) /// Sends the MAC of a device's key to the partner device. -class KeyVerificationMacEvent : public Event { - Q_GADGET +class QUOTIENT_API KeyVerificationMacEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) -- cgit v1.2.3 From 53dfa70601b2d27a6be12d52e86af123d0b26b79 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 15 Feb 2022 20:51:32 +0100 Subject: Cleanup A note on switching to QLatin1String for JSON key constants - this is more concise and barely affects (if at all) runtime performance (padding each QChar with zeros is trivial for assignment; and comparison can be done directly with the same performance as for two QStrings). --- lib/connection.cpp | 22 +++++++----- lib/e2ee/e2ee.h | 76 ++++++++++++++++------------------------ lib/e2ee/qolmaccount.cpp | 79 ++++++++++++++++-------------------------- lib/e2ee/qolminboundsession.h | 26 +++++++------- lib/e2ee/qolmmessage.cpp | 4 +-- lib/e2ee/qolmmessage.h | 3 +- lib/e2ee/qolmoutboundsession.h | 4 +-- lib/e2ee/qolmsession.h | 41 +++++++++++++++------- lib/e2ee/qolmutility.h | 3 -- lib/events/encryptedevent.cpp | 13 +++++++ lib/events/encryptedevent.h | 14 +++----- lib/syncdata.h | 2 +- 12 files changed, 138 insertions(+), 149 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 14188ace..3b8da6d1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -754,18 +754,20 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { #ifdef Quotient_E2EE_ENABLED - if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) { + const auto oneTimeKeyCount = + static_cast(data.deviceOneTimeKeysCount()[SignedCurve25519Key]); + if (oneTimeKeyCount < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() + && !d->isUploadingKeys) { d->isUploadingKeys = true; - d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() / 2 - data.deviceOneTimeKeysCount()["signed_curve25519"]); + d->olmAccount->generateOneTimeKeys( + d->olmAccount->maxNumberOfOneTimeKeys() / 2 - oneTimeKeyCount); auto keys = d->olmAccount->oneTimeKeys(); auto job = d->olmAccount->createUploadKeyRequest(keys); run(job, ForegroundRequest); - connect(job, &BaseJob::success, this, [this](){ - d->olmAccount->markKeysAsPublished(); - }); - connect(job, &BaseJob::result, this, [this](){ - d->isUploadingKeys = false; - }); + connect(job, &BaseJob::success, this, + [this] { d->olmAccount->markKeysAsPublished(); }); + connect(job, &BaseJob::result, this, + [this] { d->isUploadingKeys = false; }); } static bool first = true; if(first) { @@ -1993,7 +1995,9 @@ void Connection::Private::loadOutdatedUserDevices() deviceKeys[user].clear(); for(const auto &device : keys) { if(device.userId != user) { - qCWarning(E2EE) << "mxId mismatch during device key verification:" << device.userId << user; + qCWarning(E2EE) + << "mxId mismatch during device key verification:" + << device.userId << user; continue; } if(!device.algorithms.contains("m.olm.v1.curve25519-aes-sha2") || !device.algorithms.contains("m.megolm.v1.aes-sha2")) { diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 4c825376..e21aa87b 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -5,43 +5,34 @@ #pragma once -#include -#include #include "converters.h" +#include "quotient_common.h" + +#include #include -#include -#include -#include -#include +namespace Quotient { -#include "util.h" +constexpr auto CiphertextKeyL = "ciphertext"_ls; +constexpr auto SenderKeyKeyL = "sender_key"_ls; +constexpr auto DeviceIdKeyL = "device_id"_ls; +constexpr auto SessionIdKeyL = "session_id"_ls; -namespace Quotient { +constexpr auto AlgorithmKeyL = "algorithm"_ls; +constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +constexpr auto AlgorithmKey = "algorithm"_ls; +constexpr auto RotationPeriodMsKey = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls; + +constexpr auto Ed25519Key = "ed25519"_ls; +constexpr auto Curve25519Key = "curve25519"_ls; +constexpr auto SignedCurve25519Key = "signed_curve25519"_ls; + +constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls; +constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; -inline const auto CiphertextKeyL = "ciphertext"_ls; -inline const auto SenderKeyKeyL = "sender_key"_ls; -inline const auto DeviceIdKeyL = "device_id"_ls; -inline const auto SessionIdKeyL = "session_id"_ls; - -inline const auto AlgorithmKeyL = "algorithm"_ls; -inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; -inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; - -inline const auto AlgorithmKey = QStringLiteral("algorithm"); -inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); -inline const auto RotationPeriodMsgsKey = - QStringLiteral("rotation_period_msgs"); - -inline const auto Ed25519Key = QStringLiteral("ed25519"); -inline const auto Curve25519Key = QStringLiteral("curve25519"); -inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); -inline const auto OlmV1Curve25519AesSha2AlgoKey = - QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -inline const auto MegolmV1AesSha2AlgoKey = - QStringLiteral("m.megolm.v1.aes-sha2"); -inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, - MegolmV1AesSha2AlgoKey }; struct Unencrypted {}; struct Encrypted { QByteArray key; @@ -55,9 +46,6 @@ using QOlmSessionPtr = std::unique_ptr; class QOlmInboundGroupSession; using QOlmInboundGroupSessionPtr = std::unique_ptr; -template struct overloaded : Ts... { using Ts::operator()...; }; -template overloaded(Ts...) -> overloaded; - struct IdentityKeys { QByteArray curve25519; @@ -73,16 +61,13 @@ struct QUOTIENT_API OneTimeKeys QMap curve25519() const; //! Get a reference to the hashmap corresponding to given key type. - std::optional> get(QString keyType) const; +// std::optional> get(QString keyType) const; }; //! Struct representing the signed one-time keys. class SignedOneTimeKey { public: - SignedOneTimeKey() = default; - SignedOneTimeKey(const SignedOneTimeKey &) = default; - SignedOneTimeKey &operator=(const SignedOneTimeKey &) = default; //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. QString key; @@ -94,8 +79,7 @@ public: template <> struct JsonObjectConverter { - static void fillFrom(const QJsonObject& jo, - SignedOneTimeKey& result) + static void fillFrom(const QJsonObject& jo, SignedOneTimeKey& result) { fromJson(jo.value("key"_ls), result.key); fromJson(jo.value("signatures"_ls), result.signatures); @@ -108,24 +92,22 @@ struct JsonObjectConverter { } }; -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs); - template class asKeyValueRange { public: - asKeyValueRange(T &data) - : m_data{data} - { - } + asKeyValueRange(T& data) + : m_data { data } + {} auto begin() { return m_data.keyValueBegin(); } - auto end() { return m_data.keyValueEnd(); } private: T &m_data; }; +template +asKeyValueRange(T&) -> asKeyValueRange; } // namespace Quotient diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 34ee7ea0..9cbb14f5 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -3,53 +3,41 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "qolmaccount.h" + #include "connection.h" -#include "csapi/keys.h" -#include "e2ee/qolmutils.h" #include "e2ee/qolmutility.h" -#include -#include -#include -#include +#include "e2ee/qolmutils.h" + +#include "csapi/keys.h" + +#include using namespace Quotient; QMap OneTimeKeys::curve25519() const { - return keys[QStringLiteral("curve25519")]; + return keys[Curve25519Key]; } -std::optional> OneTimeKeys::get(QString keyType) const -{ - if (!keys.contains(keyType)) { - return std::nullopt; - } - return keys[keyType]; -} - -bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs) -{ - return lhs.curve25519 == rhs.curve25519 && lhs.ed25519 == rhs.ed25519; -} +//std::optional> OneTimeKeys::get(QString keyType) const +//{ +// if (!keys.contains(keyType)) { +// return std::nullopt; +// } +// return keys[keyType]; +//} // Convert olm error to enum QOlmError lastError(OlmAccount *account) { return fromString(olm_account_last_error(account)); } -QByteArray getRandom(size_t bufferSize) -{ - QByteArray buffer(bufferSize, '0'); - std::generate(buffer.begin(), buffer.end(), std::rand); - return buffer; -} - -QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent) +QOlmAccount::QOlmAccount(const QString& userId, const QString& deviceId, + QObject* parent) : QObject(parent) , m_userId(userId) , m_deviceId(deviceId) -{ -} +{} QOlmAccount::~QOlmAccount() { @@ -66,7 +54,7 @@ void QOlmAccount::createNewAccount() if (error == olm_error()) { throw lastError(m_account); } - Q_EMIT needsSave(); + emit needsSave(); } void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) @@ -161,7 +149,7 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const if (error == olm_error()) { throw lastError(m_account); } - Q_EMIT needsSave(); + emit needsSave(); return error; } @@ -220,14 +208,11 @@ std::optional QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &se if (error == olm_error()) { return lastError(m_account); } - Q_EMIT needsSave(); + emit needsSave(); return std::nullopt; } -OlmAccount *QOlmAccount::data() -{ - return m_account; -} +OlmAccount* QOlmAccount::data() { return m_account; } DeviceKeys QOlmAccount::deviceKeys() const { @@ -284,31 +269,27 @@ std::variant QOlmAccount::createOutboundSession(const void QOlmAccount::markKeysAsPublished() { olm_account_mark_keys_as_published(m_account); - Q_EMIT needsSave(); + emit needsSave(); } -bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys, - const QString &deviceId, - const QString &userId) +bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId) { const auto signKeyId = "ed25519:" + deviceId; const auto signingKey = deviceKeys.keys[signKeyId]; const auto signature = deviceKeys.signatures[userId][signKeyId]; - if (signature.isEmpty()) { - return false; - } - return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); } -bool Quotient::ed25519VerifySignature(const QString &signingKey, - const QJsonObject &obj, - const QString &signature) +bool Quotient::ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature) { - if (signature.isEmpty()) { + if (signature.isEmpty()) return false; - } + QJsonObject obj1 = obj; obj1.remove("unsigned"); diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 1f5dadd3..437f753d 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -4,12 +4,12 @@ #pragma once -#include -#include -#include -#include "olm/olm.h" -#include "e2ee/qolmerrors.h" #include "e2ee/e2ee.h" +#include "e2ee/qolmerrors.h" +#include "olm/olm.h" + +#include +#include namespace Quotient { @@ -20,16 +20,18 @@ class QUOTIENT_API QOlmInboundGroupSession public: ~QOlmInboundGroupSession(); //! Creates a new instance of `OlmInboundGroupSession`. - static std::unique_ptr create(const QByteArray &key); + static std::unique_ptr create(const QByteArray& key); //! Import an inbound group session, from a previous export. - static std::unique_ptr import(const QByteArray &key); + static std::unique_ptr import(const QByteArray& key); //! Serialises an `OlmInboundGroupSession` to encrypted Base64. QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant, QOlmError> unpickle(const QByteArray &picked, const PicklingMode &mode); + static std::variant, QOlmError> + unpickle(const QByteArray& picked, const PicklingMode& mode); //! Decrypts ciphertext received for this group session. - std::variant, QOlmError> decrypt(const QByteArray &message); + std::variant, QOlmError> decrypt( + const QByteArray& message); //! Export the base64-encoded ratchet key for this session, at the given index, //! in a format which can be used by import. std::variant exportSession(uint32_t messageIndex); @@ -38,11 +40,11 @@ public: //! Get a base64-encoded identifier for this session. QByteArray sessionId() const; bool isVerified() const; - QOlmInboundGroupSession(OlmInboundGroupSession *session); + + QOlmInboundGroupSession(OlmInboundGroupSession* session); private: - OlmInboundGroupSession *m_groupSession; + OlmInboundGroupSession* m_groupSession; }; using QOlmInboundGroupSessionPtr = std::unique_ptr; -using OlmInboundGroupSessionPtr = std::unique_ptr; } // namespace Quotient diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp index 15008b75..81b166b0 100644 --- a/lib/e2ee/qolmmessage.cpp +++ b/lib/e2ee/qolmmessage.cpp @@ -6,11 +6,11 @@ using namespace Quotient; -QOlmMessage::QOlmMessage(const QByteArray &ciphertext, QOlmMessage::Type type) +QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) : QByteArray(std::move(ciphertext)) , m_messageType(type) { - Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty"); + Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty"); } QOlmMessage::QOlmMessage(const QOlmMessage &message) diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h index 557c02b1..5d5db636 100644 --- a/lib/e2ee/qolmmessage.h +++ b/lib/e2ee/qolmmessage.h @@ -28,8 +28,9 @@ public: Q_ENUM(Type) QOlmMessage() = default; - explicit QOlmMessage(const QByteArray &ciphertext, Type type = General); + explicit QOlmMessage(QByteArray ciphertext, Type type = General); explicit QOlmMessage(const QOlmMessage &message); + ~QOlmMessage() = default; static QOlmMessage fromCiphertext(const QByteArray &ciphertext); diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 0122bbfd..32ba2b3b 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -24,7 +24,8 @@ public: std::variant pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode); + static std::variant, QOlmError> + unpickle(QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. std::variant encrypt(const QString &plaintext); @@ -48,5 +49,4 @@ private: }; using QOlmOutboundGroupSessionPtr = std::unique_ptr; -using OlmOutboundGroupSessionPtr = std::unique_ptr; } diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index 889a606d..f20c9837 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -16,20 +16,31 @@ namespace Quotient { class QOlmAccount; class QOlmSession; - //! Either an outbound or inbound session for secure communication. class QUOTIENT_API QOlmSession { public: ~QOlmSession(); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::variant, QOlmError> createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage); - static std::variant, QOlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage); - static std::variant, QOlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey); + static std::variant, QOlmError> + createInboundSession(QOlmAccount* account, const QOlmMessage& preKeyMessage); + + static std::variant, QOlmError> + createInboundSessionFrom(QOlmAccount* account, + const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage); + + static std::variant, QOlmError> + createOutboundSession(QOlmAccount* account, const QString& theirIdentityKey, + const QString& theirOneTimeKey); + //! Serialises an `QOlmSession` to encrypted Base64. std::variant pickle(const PicklingMode &mode); + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode); + static std::variant, QOlmError> unpickle( + const QByteArray& pickled, const PicklingMode& mode); + //! Encrypts a plaintext message using the session. QOlmMessage encrypt(const QString &plaintext); @@ -48,29 +59,33 @@ public: bool hasReceivedMessage() const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSession(const QOlmMessage &preKeyMessage) const; + std::variant matchesInboundSession( + const QOlmMessage& preKeyMessage) const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const; + std::variant matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const; friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) { return lhs.sessionId() < rhs.sessionId(); } - friend bool operator<(const std::unique_ptr &lhs, const std::unique_ptr &rhs) { + friend bool operator<(const std::unique_ptr& lhs, + const std::unique_ptr& rhs) + { return *lhs < *rhs; } - OlmSession *raw() const - { - return m_session; - } + OlmSession* raw() const { return m_session; } + QOlmSession(OlmSession* session); private: //! Helper function for creating new sessions and handling errors. static OlmSession* create(); - static std::variant, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); + static std::variant, QOlmError> createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, + bool from = false, const QString& theirIdentityKey = ""); OlmSession* m_session; }; } //namespace Quotient diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index b2e79e29..a12af49a 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -4,7 +4,6 @@ #pragma once -#include #include #include "e2ee/qolmerrors.h" @@ -13,7 +12,6 @@ struct OlmUtility; namespace Quotient { class QOlmSession; -class Connection; //! Allows you to make use of crytographic hashing via SHA-2 and //! verifying ed25519 signatures. @@ -37,7 +35,6 @@ public: std::variant ed25519Verify(const QByteArray &key, const QByteArray &message, const QByteArray &signature); - private: OlmUtility *m_utility; diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 1b5e4441..ba4dd154 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -33,6 +33,19 @@ EncryptedEvent::EncryptedEvent(const QJsonObject& obj) qCDebug(E2EE) << "Encrypted event from" << senderId(); } +QString EncryptedEvent::algorithm() const +{ + auto algo = contentPart(AlgorithmKeyL); + static constexpr auto SupportedAlgorithms = + make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); + if (std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), + algo) == SupportedAlgorithms.cend()) { + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + } + return algo; +} + RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const { auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index c838bbd8..72efffd4 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -39,22 +39,16 @@ public: const QString& deviceId, const QString& sessionId); explicit EncryptedEvent(const QJsonObject& obj); - QString algorithm() const - { - QString algo = contentPart(AlgorithmKeyL); - if (!SupportedAlgorithms.contains(algo)) { - qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo - << "is not supported"; - } - return algo; - } + QString algorithm() const; QByteArray ciphertext() const { return contentPart(CiphertextKeyL).toLatin1(); } QJsonObject ciphertext(const QString& identityKey) const { - return contentPart(CiphertextKeyL).value(identityKey).toObject(); + return contentPart(CiphertextKeyL) + .value(identityKey) + .toObject(); } QString senderKey() const { return contentPart(SenderKeyKeyL); } diff --git a/lib/syncdata.h b/lib/syncdata.h index 633f4b52..6b70140d 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -54,7 +54,7 @@ struct DevicesList { QStringList left; }; -QDebug operator<<(QDebug dhg, const DevicesList &devicesList); +QDebug operator<<(QDebug dhg, const DevicesList& devicesList); template <> struct JsonObjectConverter { -- cgit v1.2.3 From 0f6506c022ff1ccaa648ff50b81ae29f5a6f2176 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 14 Feb 2022 08:55:50 +0100 Subject: Connection: guard device loading per-object Using a static variable is incorrect as it doesn't load the device list for any subsequent created Connection object. --- lib/connection.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 3b8da6d1..0562d416 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -131,6 +131,7 @@ public: #ifdef Quotient_E2EE_ENABLED std::unique_ptr olmAccount; bool isUploadingKeys = false; + bool firstSync = true; #endif // Quotient_E2EE_ENABLED QPointer resolverJob = nullptr; @@ -769,10 +770,9 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) connect(job, &BaseJob::result, this, [this] { d->isUploadingKeys = false; }); } - static bool first = true; - if(first) { + if(d->firstSync) { d->loadDevicesList(); - first = false; + d->firstSync = false; } d->consumeDevicesList(data.takeDevicesList()); -- cgit v1.2.3 From 445e34f26450cf8262a65b74e1294579d9cd56be Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 13 Feb 2022 22:11:52 +0100 Subject: Fix file decryption --- lib/events/encryptedfile.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index 74119127..dbb72af8 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -13,7 +13,7 @@ using namespace Quotient; QByteArray EncryptedFile::decryptFile(const QByteArray &ciphertext) const { QString _key = key.k; - _key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); + auto keyBytes = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { qCWarning(E2EE) << "Hash verification failed for file"; @@ -23,7 +23,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray &ciphertext) const EVP_CIPHER_CTX *ctx; int length; ctx = EVP_CIPHER_CTX_new(); - EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)_key.data(), (const unsigned char *)iv.toLatin1().data()); + EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)keyBytes.data(), (const unsigned char *)QByteArray::fromBase64(iv.toLatin1()).data()); EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); EVP_CIPHER_CTX_free(ctx); -- cgit v1.2.3 From 2178ca994c4b33197239155f7f6715e0451b9172 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 14 Feb 2022 14:06:14 +0100 Subject: Use QHash instead of QMap We don't seem to need sorted associative containers in those cases. --- lib/e2ee/e2ee.h | 4 ++-- lib/e2ee/qolmaccount.cpp | 16 ++++------------ lib/e2ee/qolmaccount.h | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index e21aa87b..361c48ff 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -55,10 +55,10 @@ struct IdentityKeys //! Struct representing the one-time keys. struct QUOTIENT_API OneTimeKeys { - QMap> keys; + QHash> keys; //! Get the HashMap containing the curve25519 one-time keys. - QMap curve25519() const; + QHash curve25519() const; //! Get a reference to the hashmap corresponding to given key type. // std::optional> get(QString keyType) const; diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 9cbb14f5..476a60bd 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -14,7 +14,7 @@ using namespace Quotient; -QMap OneTimeKeys::curve25519() const +QHash OneTimeKeys::curve25519() const { return keys[Curve25519Key]; } @@ -164,21 +164,13 @@ OneTimeKeys QOlmAccount::oneTimeKeys() const } const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); OneTimeKeys oneTimeKeys; - - for (const QString& key1 : json.keys()) { - auto oneTimeKeyObject = json[key1].toObject(); - auto keyMap = QMap(); - for (const QString &key2 : oneTimeKeyObject.keys()) { - keyMap[key2] = oneTimeKeyObject[key2].toString(); - } - oneTimeKeys.keys[key1] = keyMap; - } + fromJson(json, oneTimeKeys.keys); return oneTimeKeys; } -QMap QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +QHash QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const { - QMap signedOneTimeKeys; + QHash signedOneTimeKeys; for (const auto &keyid : keys.curve25519().keys()) { const auto oneTimeKey = keys.curve25519()[keyid]; QByteArray sign = signOneTimeKey(oneTimeKey); diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 17aca8aa..17f43f1a 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -67,7 +67,7 @@ public: OneTimeKeys oneTimeKeys() const; //! Sign all one time keys. - QMap signOneTimeKeys(const OneTimeKeys &keys) const; + QHash signOneTimeKeys(const OneTimeKeys &keys) const; //! Sign one time key. QByteArray signOneTimeKey(const QString &key) const; -- cgit v1.2.3 From 0a43c023b94e12b3130572f2dd0d6ac8bb4ed110 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 14 Feb 2022 15:25:24 +0100 Subject: isSupportedAlgorithm() That's a better primitive than just exposing SupportedAlgorithms list. --- lib/connection.cpp | 7 +++++-- lib/e2ee/e2ee.h | 9 +++++++++ lib/events/encryptedevent.cpp | 9 +++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0562d416..87fc8e2c 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2000,8 +2000,11 @@ void Connection::Private::loadOutdatedUserDevices() << device.userId << user; continue; } - if(!device.algorithms.contains("m.olm.v1.curve25519-aes-sha2") || !device.algorithms.contains("m.megolm.v1.aes-sha2")) { - qCWarning(E2EE) << "Unsupported encryption algorithms found" << device.algorithms; + if (!std::all_of(device.algorithms.cbegin(), + device.algorithms.cend(), + isSupportedAlgorithm)) { + qCWarning(E2EE) << "Unsupported encryption algorithms found" + << device.algorithms; continue; } if(!verifyIdentitySignature(device, device.deviceId, device.userId)) { diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 361c48ff..268cb525 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -33,6 +33,15 @@ constexpr auto SignedCurve25519Key = "signed_curve25519"_ls; constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls; constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; +inline bool isSupportedAlgorithm(const QString& algorithm) +{ + static constexpr auto SupportedAlgorithms = + make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); + return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), + algorithm) + != SupportedAlgorithms.cend(); +} + struct Unencrypted {}; struct Encrypted { QByteArray key; diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index ba4dd154..9d07a35f 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -35,14 +35,11 @@ EncryptedEvent::EncryptedEvent(const QJsonObject& obj) QString EncryptedEvent::algorithm() const { - auto algo = contentPart(AlgorithmKeyL); - static constexpr auto SupportedAlgorithms = - make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); - if (std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), - algo) == SupportedAlgorithms.cend()) { + const auto algo = contentPart(AlgorithmKeyL); + if (!isSupportedAlgorithm(algo)) qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo << "is not supported"; - } + return algo; } -- cgit v1.2.3 From b5e1fc7d8fcf9336db0dfb351403aa06dcb226a0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 16 Feb 2022 08:40:56 +0100 Subject: More cleanup, especially in EncryptedFile For EncryptedFile: - JSON converter bodies moved away to .cpp; - instead of C-style casts, reinterpret_cast is used to convert from (const) char* to (const) unsigned char*; - the size for the target plain text takes into account the case where the cipher block size can be larger than 1 (after reading https://www.openssl.org/docs/man1.1.1/man3/EVP_DecryptUpdate.html). - file decryption is wrapped in #ifdef Quotient_E2EE_ENABLED, to avoid OpenSSL linking errors when compiling without E2EE. --- lib/connection.cpp | 21 ++++++++--- lib/events/encryptedfile.cpp | 89 +++++++++++++++++++++++++++++++++++++------- lib/events/encryptedfile.h | 40 +++----------------- lib/jobs/downloadfilejob.cpp | 6 +-- 4 files changed, 99 insertions(+), 57 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 87fc8e2c..1ef2495d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2007,8 +2007,10 @@ void Connection::Private::loadOutdatedUserDevices() << device.algorithms; continue; } - if(!verifyIdentitySignature(device, device.deviceId, device.userId)) { - qCWarning(E2EE) << "Failed to verify devicekeys signature. Skipping this device"; + if (!verifyIdentitySignature(device, device.deviceId, + device.userId)) { + qCWarning(E2EE) << "Failed to verify devicekeys signature. " + "Skipping this device"; continue; } deviceKeys[user][device.deviceId] = device; @@ -2022,9 +2024,11 @@ void Connection::Private::loadOutdatedUserDevices() void Connection::Private::saveDevicesList() { q->database()->transaction(); - auto query = q->database()->prepareQuery(QStringLiteral("DELETE FROM tracked_users")); + auto query = q->database()->prepareQuery( + QStringLiteral("DELETE FROM tracked_users")); q->database()->execute(query); - query.prepare(QStringLiteral("INSERT INTO tracked_users(matrixId) VALUES(:matrixId);")); + query.prepare(QStringLiteral( + "INSERT INTO tracked_users(matrixId) VALUES(:matrixId);")); for (const auto& user : trackedUsers) { query.bindValue(":matrixId", user); q->database()->execute(query); @@ -2032,13 +2036,18 @@ void Connection::Private::saveDevicesList() query.prepare(QStringLiteral("DELETE FROM outdated_users")); q->database()->execute(query); - query.prepare(QStringLiteral("INSERT INTO outdated_users(matrixId) VALUES(:matrixId);")); + query.prepare(QStringLiteral( + "INSERT INTO outdated_users(matrixId) VALUES(:matrixId);")); for (const auto& user : outdatedUsers) { query.bindValue(":matrixId", user); q->database()->execute(query); } - query.prepare(QStringLiteral("INSERT INTO tracked_devices(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);")); + query.prepare(QStringLiteral( + "INSERT INTO tracked_devices" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) " + "VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);" + )); for (const auto& user : deviceKeys.keys()) { for (const auto& device : deviceKeys[user]) { auto keys = device.keys.keys(); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index dbb72af8..d4a517bd 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -5,27 +5,88 @@ #include "encryptedfile.h" #include "logging.h" +#ifdef Quotient_E2EE_ENABLED #include #include +#endif using namespace Quotient; -QByteArray EncryptedFile::decryptFile(const QByteArray &ciphertext) const +QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const { - QString _key = key.k; - auto keyBytes = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1()); +#ifdef Quotient_E2EE_ENABLED + auto _key = key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); - if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + if (sha256 + != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { qCWarning(E2EE) << "Hash verification failed for file"; - return QByteArray(); + return {}; } - QByteArray plaintext(ciphertext.size(), 0); - EVP_CIPHER_CTX *ctx; - int length; - ctx = EVP_CIPHER_CTX_new(); - EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)keyBytes.data(), (const unsigned char *)QByteArray::fromBase64(iv.toLatin1()).data()); - EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size()); - EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext; + { + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_CIPHER_CTX_block_size(ctx) + - 1, + '\0'); + EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast( + keyBytes.data()), + reinterpret_cast( + QByteArray::fromBase64(iv.toLatin1()).data())); + EVP_DecryptUpdate( + ctx, reinterpret_cast(plaintext.data()), &length, + reinterpret_cast(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext; + } +#else + qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " + "cannot decrypt the file"; + return ciphertext; +#endif +} + +void JsonObjectConverter::dumpTo(QJsonObject& jo, + const EncryptedFile& pod) +{ + addParam<>(jo, QStringLiteral("url"), pod.url); + addParam<>(jo, QStringLiteral("key"), pod.key); + addParam<>(jo, QStringLiteral("iv"), pod.iv); + addParam<>(jo, QStringLiteral("hashes"), pod.hashes); + addParam<>(jo, QStringLiteral("v"), pod.v); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, + EncryptedFile& pod) +{ + fromJson(jo.value("url"_ls), pod.url); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("iv"_ls), pod.iv); + fromJson(jo.value("hashes"_ls), pod.hashes); + fromJson(jo.value("v"_ls), pod.v); +} + +void JsonObjectConverter::dumpTo(QJsonObject &jo, const JWK &pod) +{ + addParam<>(jo, QStringLiteral("kty"), pod.kty); + addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); + addParam<>(jo, QStringLiteral("alg"), pod.alg); + addParam<>(jo, QStringLiteral("k"), pod.k); + addParam<>(jo, QStringLiteral("ext"), pod.ext); +} + +void JsonObjectConverter::fillFrom(const QJsonObject &jo, JWK &pod) +{ + fromJson(jo.value("kty"_ls), pod.kty); + fromJson(jo.value("key_ops"_ls), pod.keyOps); + fromJson(jo.value("alg"_ls), pod.alg); + fromJson(jo.value("k"_ls), pod.k); + fromJson(jo.value("ext"_ls), pod.ext); } diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 43bafc49..0558563f 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -49,42 +49,14 @@ public: }; template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const EncryptedFile& pod) - { - addParam<>(jo, QStringLiteral("url"), pod.url); - addParam<>(jo, QStringLiteral("key"), pod.key); - addParam<>(jo, QStringLiteral("iv"), pod.iv); - addParam<>(jo, QStringLiteral("hashes"), pod.hashes); - addParam<>(jo, QStringLiteral("v"), pod.v); - } - static void fillFrom(const QJsonObject& jo, EncryptedFile& pod) - { - fromJson(jo.value("url"_ls), pod.url); - fromJson(jo.value("key"_ls), pod.key); - fromJson(jo.value("iv"_ls), pod.iv); - fromJson(jo.value("hashes"_ls), pod.hashes); - fromJson(jo.value("v"_ls), pod.v); - } +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const EncryptedFile& pod); + static void fillFrom(const QJsonObject& jo, EncryptedFile& pod); }; template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const JWK& pod) - { - addParam<>(jo, QStringLiteral("kty"), pod.kty); - addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); - addParam<>(jo, QStringLiteral("alg"), pod.alg); - addParam<>(jo, QStringLiteral("k"), pod.k); - addParam<>(jo, QStringLiteral("ext"), pod.ext); - } - static void fillFrom(const QJsonObject& jo, JWK& pod) - { - fromJson(jo.value("kty"_ls), pod.kty); - fromJson(jo.value("key_ops"_ls), pod.keyOps); - fromJson(jo.value("alg"_ls), pod.alg); - fromJson(jo.value("k"_ls), pod.k); - fromJson(jo.value("ext"_ls), pod.ext); - } +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const JWK& pod); + static void fillFrom(const QJsonObject& jo, JWK& pod); }; } // namespace Quotient diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 634e5fb9..d00fc5f4 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -127,7 +127,7 @@ BaseJob::Status DownloadFileJob::prepareResult() QByteArray encrypted = d->tempFile->readAll(); EncryptedFile file = *d->encryptedFile; - auto decrypted = file.decryptFile(encrypted); + const auto decrypted = file.decryptFile(encrypted); d->targetFile->write(decrypted); d->tempFile->remove(); } else { @@ -149,10 +149,10 @@ BaseJob::Status DownloadFileJob::prepareResult() #ifdef Quotient_E2EE_ENABLED if (d->encryptedFile.has_value()) { d->tempFile->seek(0); - auto encrypted = d->tempFile->readAll(); + const auto encrypted = d->tempFile->readAll(); EncryptedFile file = *d->encryptedFile; - auto decrypted = file.decryptFile(encrypted); + const auto decrypted = file.decryptFile(encrypted); d->tempFile->write(decrypted); } else { #endif -- cgit v1.2.3 From ba2dc2a1aa23c91a52bcd0d9f3feb29ea9ecfcd2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 16 Feb 2022 10:31:37 +0100 Subject: ci.yml: enable E2EE pipelines again The whole cycle will get terribly long again; looking forward to parallel quotest... --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d902bd3..c8ea0d95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,18 +19,20 @@ jobs: fail-fast: false max-parallel: 1 matrix: - os: [ubuntu-20.04, macos-10.15] + os: [ ubuntu-20.04, macos-10.15 ] compiler: [ GCC, Clang ] qt-version: [ '5.12.12' ] # Not using binary values here, to make the job captions more readable - e2ee: [ '' ] - update-api: [ '', 'update-api' ] + e2ee: [ '', e2ee ] + update-api: [ '', update-api ] sonar: [ '' ] platform: [ '' ] qt-arch: [ '' ] exclude: - os: macos-10.15 compiler: GCC + - os: windows-2019 + e2ee: e2ee # Not supported by the current CI script include: - os: ubuntu-latest compiler: GCC @@ -55,7 +57,6 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - submodules: ${{ matrix.e2ee != '' }} - name: Cache Qt id: cache-qt @@ -154,7 +155,6 @@ jobs: if: ${{ contains(matrix.os, 'ubuntu') && matrix.e2ee }} run: | sudo apt-get install libssl-dev - echo "openssl version" >>$GITHUB_ENV - name: Build and install olm if: matrix.e2ee -- cgit v1.2.3 From e93260a6b68279518270d0aec2c55f826375fa8a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 16 Feb 2022 12:25:10 +0100 Subject: quotient_export.h: #include To ensure Q_DECL_EXPORT/Q_DECL_IMPORT macros are defined. --- lib/quotient_export.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/quotient_export.h b/lib/quotient_export.h index 5a6edb0e..56767443 100644 --- a/lib/quotient_export.h +++ b/lib/quotient_export.h @@ -3,6 +3,8 @@ #pragma once +#include + #ifdef QUOTIENT_STATIC # define QUOTIENT_API # define QUOTIENT_HIDDEN -- cgit v1.2.3 From a42fd5d188e58da95169ea21b4cae23cccd26819 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 16 Feb 2022 17:28:29 +0100 Subject: TestOlmUtility: fix building with Qt 5.12 QKeyValueIterator::operator->() only arrived in Qt 5.15. --- autotests/testolmutility.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index d0476af0..7a55b61e 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -52,7 +52,7 @@ void TestOlmUtility::verifySignedOneTimeKey() aliceOlm.generateOneTimeKeys(1); auto keys = aliceOlm.oneTimeKeys(); - auto firstKey = keys.curve25519().keyValueBegin()->second; + auto firstKey = *keys.curve25519().begin(); auto msgObj = QJsonObject({{"key", firstKey}}); auto sig = aliceOlm.sign(msgObj); -- cgit v1.2.3 From 6a7e21cfc542588d6bcf7efe839439bf9b632600 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 17 Feb 2022 00:29:06 +0100 Subject: Don't create QApplications in tests --- autotests/testgroupsession.cpp | 2 +- autotests/testolmaccount.cpp | 2 +- autotests/testolmsession.cpp | 2 +- autotests/testolmutility.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 5024ccea..4425a006 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -51,4 +51,4 @@ void TestOlmSession::groupSessionCryptoValid() QCOMPARE(0, decryptionResult.second); } -QTEST_MAIN(TestOlmSession) +QTEST_APPLESS_MAIN(TestOlmSession) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 62b786d0..37d9f1d1 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -573,4 +573,4 @@ void TestOlmAccount::enableEncryption() QVERIFY(spy2.wait(10000)); } -QTEST_MAIN(TestOlmAccount) +QTEST_APPLESS_MAIN(TestOlmAccount) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 41baf8e3..12279cb6 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -77,4 +77,4 @@ void TestOlmSession::correctSessionOrdering() QCOMPARE(sessionList[2]->sessionId(), session1Id); } -QTEST_MAIN(TestOlmSession) +QTEST_APPLESS_MAIN(TestOlmSession) diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 7a55b61e..ca5aa1fd 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -126,4 +126,4 @@ void TestOlmUtility::validUploadKeysRequest() QVERIFY(verifyIdentitySignature(deviceKeys, deviceId, userId)); } -QTEST_MAIN(TestOlmUtility) +QTEST_APPLESS_MAIN(TestOlmUtility) -- cgit v1.2.3 From 120524a139dbac5d55c952de5dba0e23ba1025f8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 10:44:38 +0100 Subject: CI: setup mock Synapse before running ctest To use this in CI required extending/fixing autotests/run-tests.sh: it can now accept arguments that are further passed to ctest invocation, and it no more cd's to the build directory because build directories can be in all kinds of places, expecting the caller to pick the directory upfront. --- .github/workflows/ci.yml | 2 +- autotests/run-tests.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8ea0d95..67ef5ac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,7 @@ jobs: QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true;quotient.events.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | - ctest --test-dir $BUILD_PATH --output-on-failure + autotests/run-tests.sh --test-dir $BUILD_PATH --output-on-failure [[ -z "$TEST_USER" ]] || \ LD_LIBRARY_PATH=$LIB_PATH \ $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index b49f37a1..adfb4ec1 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -18,6 +18,6 @@ docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -p secre echo Register carl docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' -cd build/ && GTEST_COLOR=1 ctest --verbose +GTEST_COLOR=1 ctest --verbose "$@" rm -rf ./data/* docker rm -f synapse 2>&1>/dev/null -- cgit v1.2.3 From 6be59df7036f8df385da29051c5320563518728a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 11:06:49 +0100 Subject: Use QCoreApplication in autotests QEventLoop refuses to work without an application object instance. --- autotests/testgroupsession.cpp | 2 +- autotests/testolmaccount.cpp | 2 +- autotests/testolmsession.cpp | 2 +- autotests/testolmutility.cpp | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 4425a006..2b949578 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -51,4 +51,4 @@ void TestOlmSession::groupSessionCryptoValid() QCOMPARE(0, decryptionResult.second); } -QTEST_APPLESS_MAIN(TestOlmSession) +QTEST_GUILESS_MAIN(TestOlmSession) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 37d9f1d1..760276c8 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -573,4 +573,4 @@ void TestOlmAccount::enableEncryption() QVERIFY(spy2.wait(10000)); } -QTEST_APPLESS_MAIN(TestOlmAccount) +QTEST_GUILESS_MAIN(TestOlmAccount) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 12279cb6..5436c392 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -77,4 +77,4 @@ void TestOlmSession::correctSessionOrdering() QCOMPARE(sessionList[2]->sessionId(), session1Id); } -QTEST_APPLESS_MAIN(TestOlmSession) +QTEST_GUILESS_MAIN(TestOlmSession) diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index ca5aa1fd..b4532c8d 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -125,5 +125,4 @@ void TestOlmUtility::validUploadKeysRequest() QVERIFY(verifyIdentitySignature(fromJson(body), deviceId, userId)); QVERIFY(verifyIdentitySignature(deviceKeys, deviceId, userId)); } - -QTEST_APPLESS_MAIN(TestOlmUtility) +QTEST_GUILESS_MAIN(TestOlmUtility) -- cgit v1.2.3 From 0a1b8b9f38ddadb32d54884a05f0e7246d7c25e7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 14:14:15 +0100 Subject: run-tests.sh: use a trap for cleanup This both is more reliable (GHA executes scripts in fail-fast mode) and ensures that the return value is that of ctest. --- autotests/run-tests.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index adfb4ec1..0d0a4ca3 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -4,11 +4,13 @@ docker run -v `pwd`/data:/data --rm \ -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:v1.24.0 generate ./.ci/adjust-config.sh docker run -d \ - --name synapse \ - -p 1234:8008 \ - -p 8448:8008 \ - -p 8008:8008 \ - -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0 + --name synapse \ + -p 1234:8008 \ + -p 8448:8008 \ + -p 8008:8008 \ + -v `pwd`/data:/data matrixdotorg/synapse:v1.24.0 +trap "rm -rf ./data/*; docker rm -f synapse 2>&1 >/dev/null; trap - EXIT" EXIT + echo Waiting for synapse to start... until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done echo Register alice @@ -19,5 +21,3 @@ echo Register carl docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' GTEST_COLOR=1 ctest --verbose "$@" -rm -rf ./data/* -docker rm -f synapse 2>&1>/dev/null -- cgit v1.2.3 From 4f4c04c53ea27777e8cf4ecab690985ad3f78e1f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 14:18:14 +0100 Subject: testgroupsession.*: fix TestOlmSession copy-pasta --- autotests/testgroupsession.cpp | 6 +++--- autotests/testgroupsession.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 2b949578..2566669e 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -9,7 +9,7 @@ using namespace Quotient; -void TestOlmSession::groupSessionPicklingValid() +void TestGroupSession::groupSessionPicklingValid() { auto ogs = QOlmOutboundGroupSession::create(); const auto ogsId = ogs->sessionId(); @@ -33,7 +33,7 @@ void TestOlmSession::groupSessionPicklingValid() QCOMPARE(igsId, igs->sessionId()); } -void TestOlmSession::groupSessionCryptoValid() +void TestGroupSession::groupSessionCryptoValid() { auto ogs = QOlmOutboundGroupSession::create(); auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); @@ -51,4 +51,4 @@ void TestOlmSession::groupSessionCryptoValid() QCOMPARE(0, decryptionResult.second); } -QTEST_GUILESS_MAIN(TestOlmSession) +QTEST_GUILESS_MAIN(TestGroupSession) diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h index 7743295f..6edf0d16 100644 --- a/autotests/testgroupsession.h +++ b/autotests/testgroupsession.h @@ -4,7 +4,7 @@ #include -class TestOlmSession : public QObject +class TestGroupSession : public QObject { Q_OBJECT -- cgit v1.2.3 From d4f1b09476c4bcc5c33cc40e2506d7fd6a731456 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 14:27:24 +0100 Subject: TestOlmAccount: align homeserver address with that in run-tests.sh It would probably be even better to pass the homeserver address in the environment but that's a bigger endeavour. Also: reformatted CREATE_CONNECTION macro. --- autotests/testolmaccount.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 760276c8..0c44c8d7 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -168,25 +168,25 @@ void TestOlmAccount::encryptedFile() QCOMPARE(file.key.kty, "oct"); } -#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ - NetworkAccessManager::instance()->ignoreSslErrors(true); \ - auto VAR = std::make_shared(); \ - (VAR) ->resolveServer("@alice:localhost:443"); \ - connect( (VAR) .get(), &Connection::loginFlowsChanged, this, [=] { \ - (VAR) ->loginWithPassword( (USERNAME) , SECRET , DEVICE_NAME , ""); \ - }); \ - connect( (VAR) .get(), &Connection::networkError, [](QString error) { \ - QWARN(qUtf8Printable(error)); \ - QFAIL("Network error: make sure synapse is running"); \ - }); \ - connect( (VAR) .get(), &Connection::loginError, [](QString error) { \ - QWARN(qUtf8Printable(error)); \ - QFAIL("Login failed"); \ - }); \ - QSignalSpy spy ## VAR ((VAR).get(), &Connection::loginFlowsChanged); \ - QSignalSpy spy2 ## VAR ((VAR).get(), &Connection::connected); \ - QVERIFY(spy ## VAR .wait(10000)); \ - QVERIFY(spy2 ## VAR .wait(10000)); +#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ + NetworkAccessManager::instance()->ignoreSslErrors(true); \ + auto VAR = std::make_shared(); \ + (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \ + connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \ + (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \ + }); \ + connect((VAR).get(), &Connection::networkError, [](QString error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Network error: make sure synapse is running"); \ + }); \ + connect((VAR).get(), &Connection::loginError, [](QString error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Login failed"); \ + }); \ + QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \ + QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \ + QVERIFY(spy##VAR.wait(10000)); \ + QVERIFY(spy2##VAR.wait(10000)); void TestOlmAccount::uploadIdentityKey() { -- cgit v1.2.3 From 34d5ed388caf8a39327c2ea4da5de197faf5583d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 16:41:27 +0100 Subject: testolmaccount.cpp: cleanup --- autotests/testolmaccount.cpp | 164 ++++++++++++++++++++++--------------------- 1 file changed, 83 insertions(+), 81 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 0c44c8d7..b877a692 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -4,13 +4,15 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testolmaccount.h" + +#include +#include #include #include -#include +#include #include #include #include -#include using namespace Quotient; @@ -84,8 +86,8 @@ void TestOlmAccount::deviceKeys() {"ed25519:JLAFKJWSCS", "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"}}; // TODO that should be the default value - device1.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2"}; + device1.algorithms = + QStringList { OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey }; device1.signatures = { {"@alice:example.com", @@ -168,24 +170,24 @@ void TestOlmAccount::encryptedFile() QCOMPARE(file.key.kty, "oct"); } -#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ - NetworkAccessManager::instance()->ignoreSslErrors(true); \ - auto VAR = std::make_shared(); \ - (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \ - connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \ - (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \ - }); \ - connect((VAR).get(), &Connection::networkError, [](QString error) { \ - QWARN(qUtf8Printable(error)); \ - QFAIL("Network error: make sure synapse is running"); \ - }); \ - connect((VAR).get(), &Connection::loginError, [](QString error) { \ - QWARN(qUtf8Printable(error)); \ - QFAIL("Login failed"); \ - }); \ - QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \ - QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \ - QVERIFY(spy##VAR.wait(10000)); \ +#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ + NetworkAccessManager::instance()->ignoreSslErrors(true); \ + auto VAR = std::make_shared(); \ + (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \ + connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \ + (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \ + }); \ + connect((VAR).get(), &Connection::networkError, [](const QString& error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Network error: make sure synapse is running"); \ + }); \ + connect((VAR).get(), &Connection::loginError, [](const QString& error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Login failed"); \ + }); \ + QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \ + QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \ + QVERIFY(spy##VAR.wait(10000)); \ QVERIFY(spy2##VAR.wait(10000)); void TestOlmAccount::uploadIdentityKey() @@ -228,7 +230,7 @@ void TestOlmAccount::uploadOneTimeKeys() auto request = new UploadKeysJob(none, oneTimeKeysHash); connect(request, &BaseJob::result, this, [request, conn] { QCOMPARE(request->oneTimeKeyCounts().size(), 1); - QCOMPARE(request->oneTimeKeyCounts()["curve25519"], 5); + QCOMPARE(request->oneTimeKeyCounts().value(Curve25519Key), 5); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -256,7 +258,7 @@ void TestOlmAccount::uploadSignedOneTimeKeys() auto request = new UploadKeysJob(none, oneTimeKeysHash); connect(request, &BaseJob::result, this, [request, nKeys, conn] { QCOMPARE(request->oneTimeKeyCounts().size(), 1); - QCOMPARE(request->oneTimeKeyCounts()["signed_curve25519"], nKeys); + QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), nKeys); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -276,7 +278,7 @@ void TestOlmAccount::uploadKeys() auto request = olmAccount->createUploadKeyRequest(otks); connect(request, &BaseJob::result, this, [request, conn] { QCOMPARE(request->oneTimeKeyCounts().size(), 1); - QCOMPARE(request->oneTimeKeyCounts()["signed_curve25519"], 1); + QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); connect(request, &BaseJob::failure, this, [] { QFAIL("upload failed"); @@ -297,7 +299,7 @@ void TestOlmAccount::queryTest() auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); connect(aliceRes, &BaseJob::result, this, [aliceRes] { QCOMPARE(aliceRes->oneTimeKeyCounts().size(), 1); - QCOMPARE(aliceRes->oneTimeKeyCounts()["signed_curve25519"], 1); + QCOMPARE(aliceRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); QSignalSpy spy(aliceRes, &BaseJob::result); alice->run(aliceRes); @@ -308,7 +310,7 @@ void TestOlmAccount::queryTest() auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); connect(bobRes, &BaseJob::result, this, [bobRes] { QCOMPARE(bobRes->oneTimeKeyCounts().size(), 1); - QCOMPARE(bobRes->oneTimeKeyCounts()["signed_curve25519"], 1); + QCOMPARE(bobRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); QSignalSpy spy1(bobRes, &BaseJob::result); bob->run(bobRes); @@ -320,13 +322,13 @@ void TestOlmAccount::queryTest() deviceKeys[bob->userId()] = QStringList(); auto job = alice->callApi(deviceKeys); QSignalSpy spy(job, &BaseJob::result); - connect(job, &BaseJob::result, this, [job, &bob, &bobOlm] { + connect(job, &BaseJob::result, this, [job, bob, bobOlm] { QCOMPARE(job->failures().size(), 0); - auto aliceDevices = job->deviceKeys()[bob->userId()]; - QVERIFY(aliceDevices.size() > 0); + const auto& aliceDevices = job->deviceKeys().value(bob->userId()); + QVERIFY(!aliceDevices.empty()); - auto devKeys = aliceDevices[bob->deviceId()]; + const auto& devKeys = aliceDevices.value(bob->deviceId()); QCOMPARE(devKeys.userId, bob->userId()); QCOMPARE(devKeys.deviceId, bob->deviceId()); QCOMPARE(devKeys.keys, bobOlm->deviceKeys().keys); @@ -340,11 +342,11 @@ void TestOlmAccount::queryTest() deviceKeys[alice->userId()] = QStringList(); auto job = bob->callApi(deviceKeys); QSignalSpy spy(job, &BaseJob::result); - connect(job, &BaseJob::result, this, [job, &alice, &aliceOlm] { + connect(job, &BaseJob::result, this, [job, alice, aliceOlm] { QCOMPARE(job->failures().size(), 0); - auto bobDevices = job->deviceKeys()[alice->userId()]; - QVERIFY(bobDevices.size() > 0); + const auto& bobDevices = job->deviceKeys().value(alice->userId()); + QVERIFY(!bobDevices.empty()); auto devKeys = bobDevices[alice->deviceId()]; QCOMPARE(devKeys.userId, alice->userId()); @@ -368,7 +370,7 @@ void TestOlmAccount::claimKeys() connect(request, &BaseJob::result, this, [request, bob] { QCOMPARE(request->oneTimeKeyCounts().size(), 1); - QCOMPARE(request->oneTimeKeyCounts()["signed_curve25519"], 1); + QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); bob->run(request); @@ -376,51 +378,47 @@ void TestOlmAccount::claimKeys() QVERIFY(requestSpy.wait(10000)); // Alice retrieves bob's keys & claims one signed one-time key. - auto *aliceOlm = alice->olmAccount(); QHash deviceKeys; deviceKeys[bob->userId()] = QStringList(); auto job = alice->callApi(deviceKeys); - connect(job, &BaseJob::result, this, [bob, alice, aliceOlm, job, this] { - auto bobDevices = job->deviceKeys()[bob->userId()]; - QVERIFY(bobDevices.size() > 0); + connect(job, &BaseJob::result, this, [bob, alice, job, this] { + const auto& bobDevices = job->deviceKeys().value(bob->userId()); + QVERIFY(!bobDevices.empty()); // Retrieve the identity key for the current device. - auto bobEd25519 = - bobDevices[bob->deviceId()].keys["ed25519:" + bob->deviceId()]; + const auto& bobEd25519 = + bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()]; const auto currentDevice = bobDevices[bob->deviceId()]; // Verify signature. - QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), bob->userId())); + QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), + bob->userId())); QHash> oneTimeKeys; oneTimeKeys[bob->userId()] = QHash(); oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; auto job = alice->callApi(oneTimeKeys); - connect(job, &BaseJob::result, this, [aliceOlm, bob, bobEd25519, job] { + connect(job, &BaseJob::result, this, [bob, bobEd25519, job] { const auto userId = bob->userId(); const auto deviceId = bob->deviceId(); // The device exists. QCOMPARE(job->oneTimeKeys().size(), 1); - QCOMPARE(job->oneTimeKeys()[userId].size(), 1); + QCOMPARE(job->oneTimeKeys().value(userId).size(), 1); // The key is the one bob sent. - auto oneTimeKey = job->oneTimeKeys()[userId][deviceId]; + const auto& oneTimeKey = + job->oneTimeKeys().value(userId).value(deviceId); QVERIFY(oneTimeKey.canConvert()); - QVariantMap varMap = oneTimeKey.toMap(); - bool found = false; - for (const auto &key : varMap.keys()) { - if (key.startsWith(QStringLiteral("signed_curve25519"))) { - found = true; - } - } - QVERIFY(found); - - //auto algo = oneTimeKey.begin().key(); - //auto contents = oneTimeKey.begin().value(); + const auto varMap = oneTimeKey.toMap(); + QVERIFY(std::any_of(varMap.constKeyValueBegin(), + varMap.constKeyValueEnd(), [](const auto& kv) { + return kv.first.startsWith( + SignedCurve25519Key); + })); }); }); } @@ -438,7 +436,7 @@ void TestOlmAccount::claimMultipleKeys() QSignalSpy spy(res, &BaseJob::result); connect(res, &BaseJob::result, this, [res] { QCOMPARE(res->oneTimeKeyCounts().size(), 1); - QCOMPARE(res->oneTimeKeyCounts()["signed_curve25519"], 10); + QCOMPARE(res->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice->run(res); @@ -448,7 +446,7 @@ void TestOlmAccount::claimMultipleKeys() QSignalSpy spy1(res1, &BaseJob::result); connect(res1, &BaseJob::result, this, [res1] { QCOMPARE(res1->oneTimeKeyCounts().size(), 1); - QCOMPARE(res1->oneTimeKeyCounts()["signed_curve25519"], 10); + QCOMPARE(res1->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice1->run(res1); @@ -458,7 +456,7 @@ void TestOlmAccount::claimMultipleKeys() QSignalSpy spy2(res2, &BaseJob::result); connect(res2, &BaseJob::result, this, [res2] { QCOMPARE(res2->oneTimeKeyCounts().size(), 1); - QCOMPARE(res2->oneTimeKeyCounts()["signed_curve25519"], 10); + QCOMPARE(res2->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice2->run(res2); @@ -482,11 +480,10 @@ void TestOlmAccount::claimMultipleKeys() auto job = bob->callApi(oneTimeKeys); connect(job, &BaseJob::result, this, [bob, job] { const auto userId = bob->userId(); - const auto deviceId = bob->deviceId(); // The device exists. QCOMPARE(job->oneTimeKeys().size(), 1); - QCOMPARE(job->oneTimeKeys()[userId].size(), 3); + QCOMPARE(job->oneTimeKeys().value(userId).size(), 3); }); } @@ -494,8 +491,9 @@ void TestOlmAccount::keyChange() { CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), QStringList()); + auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, {}); connect(job, &BaseJob::result, this, [alice, job, this] { + QVERIFY(job->status().good()); // Alice syncs to get the first next_batch token. alice->sync(); connect(alice.get(), &Connection::syncDone, this, [alice, this] { @@ -517,7 +515,7 @@ void TestOlmAccount::keyChange() connect(changeJob, &BaseJob::result, this, [changeJob, alice] { QCOMPARE(changeJob->changed().size(), 1); QCOMPARE(changeJob->left().size(), 0); - QCOMPARE(changeJob->changed()[0], alice->userId()); + QCOMPARE(*changeJob->changed().cbegin(), alice->userId()); }); QSignalSpy spy2(changeJob, &BaseJob::result); QVERIFY(spy2.wait(10000)); @@ -534,39 +532,43 @@ void TestOlmAccount::enableEncryption() CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") - QString joinedRoom; + QString joinedRoomId; - auto job = alice->createRoom(Connection::PublishRoom, QString(), QString(), QString(), {"@bob:localhost"}); - connect(alice.get(), &Connection::newRoom, this, [alice, bob, &joinedRoom, this] (Quotient::Room *room) { - room->activateEncryption(); - QSignalSpy spy(room, &Room::encryption); + auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, + { "@bob:localhost" }); + connect(alice.get(), &Connection::newRoom, this, + [alice, bob, &joinedRoomId](Quotient::Room* room) { + room->activateEncryption(); + QSignalSpy spy(room, &Room::encryption); - joinedRoom = room->id(); - auto job = bob->joinRoom(room->id()); - QSignalSpy spy1(job, &BaseJob::result); - QVERIFY(spy.wait(10000)); - QVERIFY(spy1.wait(10000)); - }); + joinedRoomId = room->id(); + auto job = bob->joinRoom(room->id()); + QSignalSpy spy1(job, &BaseJob::result); + QVERIFY(spy.wait(10000)); + QVERIFY(spy1.wait(10000)); + }); QSignalSpy spy(job, &BaseJob::result); QVERIFY(spy.wait(10000)); bob->sync(); - connect(bob.get(), &Connection::syncDone, this, [bob, joinedRoom, this] { - auto &events = bob->room(joinedRoom)->messageEvents(); + connect(bob.get(), &Connection::syncDone, this, [bob, joinedRoomId] { + const auto* joinedRoom = bob->room(joinedRoomId); + const auto& events = joinedRoom->messageEvents(); bool hasEncryption = false; for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto event = it->event(); + const auto& event = it->event(); if (eventCast(event)) { hasEncryption = true; } else { - qDebug() << event->matrixType() << typeId() << event->type(); - if ( event->matrixType() == "m.room.encryption") { + qDebug() << event->matrixType() << typeId() + << event->type(); + if (is(*event)) { qDebug() << event->contentJson(); - } + } } } - QVERIFY(bob->room(joinedRoom)->usesEncryption()); + QVERIFY(bob->room(joinedRoomId)->usesEncryption()); QVERIFY(hasEncryption); }); QSignalSpy spy2(bob.get(), &Connection::syncDone); -- cgit v1.2.3 From c239efb530d984bcfc19a02c96bfb5481adb0971 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 16:42:42 +0100 Subject: TestOlmAccount::claimKeys(): auth bob as bob, not as alice --- autotests/testolmaccount.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index b877a692..36f10f2e 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -361,7 +361,7 @@ void TestOlmAccount::queryTest() void TestOlmAccount::claimKeys() { CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - CREATE_CONNECTION(bob, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") // Bob uploads his keys. auto *bobOlm = bob->olmAccount(); -- cgit v1.2.3 From d7ecf9b839b841ca22a3611b3608801a811e7b00 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 18 Feb 2022 23:10:34 +0100 Subject: Only testolmaccount on Linux On other platforms Docker fails to stand up the container. --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67ef5ac5..f622662b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,12 @@ jobs: QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true;quotient.events.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | - autotests/run-tests.sh --test-dir $BUILD_PATH --output-on-failure + CTEST_ARGS="--test-dir $BUILD_PATH --output-on-failure" + if [[ -z '${{ matrix.e2ee }}' || '${{ runner.os }}' != 'Linux' ]]; then + ctest $CTEST_ARGS -E testolmaccount + else + autotests/run-tests.sh $CTEST_ARGS + fi [[ -z "$TEST_USER" ]] || \ LD_LIBRARY_PATH=$LIB_PATH \ $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" -- cgit v1.2.3 From fb406897292184c46432ed0247ca9bdbef666f69 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 23 Feb 2022 01:37:02 +0100 Subject: Fix tests a bit --- autotests/run-tests.sh | 16 +++++++-- autotests/testolmaccount.cpp | 79 ++++++++++---------------------------------- autotests/testolmaccount.h | 1 - 3 files changed, 31 insertions(+), 65 deletions(-) diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index 0d0a4ca3..74a8c544 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -1,5 +1,6 @@ mkdir -p data chmod 0777 data +rm ~/.local/share/testolmaccount -rf docker run -v `pwd`/data:/data --rm \ -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:v1.24.0 generate ./.ci/adjust-config.sh @@ -14,9 +15,20 @@ trap "rm -rf ./data/*; docker rm -f synapse 2>&1 >/dev/null; trap - EXIT" EXIT echo Waiting for synapse to start... until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done echo Register alice -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice1 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice2 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice3 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice4 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice5 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice6 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice7 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice8 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice9 -p secret -c /data/homeserver.yaml https://localhost:8008' echo Register bob -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob1 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob2 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob3 -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob4 -p secret -c /data/homeserver.yaml https://localhost:8008' echo Register carl docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 36f10f2e..033fff32 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -192,7 +192,7 @@ void TestOlmAccount::encryptedFile() void TestOlmAccount::uploadIdentityKey() { - CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(conn, "alice1", "secret", "AlicePhone") auto olmAccount = conn->olmAccount(); auto idKeys = olmAccount->identityKeys(); @@ -214,7 +214,7 @@ void TestOlmAccount::uploadIdentityKey() void TestOlmAccount::uploadOneTimeKeys() { - CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(conn, "alice2", "secret", "AlicePhone") auto olmAccount = conn->olmAccount(); auto nKeys = olmAccount->generateOneTimeKeys(5); @@ -242,7 +242,7 @@ void TestOlmAccount::uploadOneTimeKeys() void TestOlmAccount::uploadSignedOneTimeKeys() { - CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(conn, "alice3", "secret", "AlicePhone") auto olmAccount = conn->olmAccount(); auto nKeys = olmAccount->generateOneTimeKeys(5); QCOMPARE(nKeys, 5); @@ -270,7 +270,7 @@ void TestOlmAccount::uploadSignedOneTimeKeys() void TestOlmAccount::uploadKeys() { - CREATE_CONNECTION(conn, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(conn, "alice4", "secret", "AlicePhone") auto olmAccount = conn->olmAccount(); auto idks = olmAccount->identityKeys(); olmAccount->generateOneTimeKeys(1); @@ -290,8 +290,8 @@ void TestOlmAccount::uploadKeys() void TestOlmAccount::queryTest() { - CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + CREATE_CONNECTION(alice, "alice5", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob1", "secret", "BobPhone") // Create and upload keys for both users. auto aliceOlm = alice->olmAccount(); @@ -360,8 +360,8 @@ void TestOlmAccount::queryTest() void TestOlmAccount::claimKeys() { - CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + CREATE_CONNECTION(alice, "alice6", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob2", "secret", "BobPhone") // Bob uploads his keys. auto *bobOlm = bob->olmAccount(); @@ -426,9 +426,9 @@ void TestOlmAccount::claimKeys() void TestOlmAccount::claimMultipleKeys() { // Login with alice multiple times - CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - CREATE_CONNECTION(alice1, "alice", "secret", "AlicePhone") - CREATE_CONNECTION(alice2, "alice", "secret", "AlicePhone") + CREATE_CONNECTION(alice, "alice7", "secret", "AlicePhone") + CREATE_CONNECTION(alice1, "alice7", "secret", "AlicePhone") + CREATE_CONNECTION(alice2, "alice7", "secret", "AlicePhone") auto olm = alice->olmAccount(); olm->generateOneTimeKeys(10); @@ -465,7 +465,7 @@ void TestOlmAccount::claimMultipleKeys() QVERIFY(spy2.wait(1000)); // TODO this is failing even with 10000 // Bob will claim all keys from alice - CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + CREATE_CONNECTION(bob, "bob3", "secret", "BobPhone") QStringList devices_; devices_ << alice->deviceId() @@ -487,50 +487,10 @@ void TestOlmAccount::claimMultipleKeys() }); } -void TestOlmAccount::keyChange() -{ - CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - - auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, {}); - connect(job, &BaseJob::result, this, [alice, job, this] { - QVERIFY(job->status().good()); - // Alice syncs to get the first next_batch token. - alice->sync(); - connect(alice.get(), &Connection::syncDone, this, [alice, this] { - const auto nextBatchToken = alice->nextBatchToken(); - - // generate keys and change existing one - auto aliceOlm = alice->olmAccount(); - aliceOlm->generateOneTimeKeys(1); - auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); - QSignalSpy spy(aliceRes, &BaseJob::result); - - alice->run(aliceRes); - QVERIFY(spy.wait(10000)); - - // The key changes should contain her username - // because of the key uploading. - - auto changeJob = alice->callApi(nextBatchToken, ""); - connect(changeJob, &BaseJob::result, this, [changeJob, alice] { - QCOMPARE(changeJob->changed().size(), 1); - QCOMPARE(changeJob->left().size(), 0); - QCOMPARE(*changeJob->changed().cbegin(), alice->userId()); - }); - QSignalSpy spy2(changeJob, &BaseJob::result); - QVERIFY(spy2.wait(10000)); - }); - QSignalSpy spy2(alice.get(), &Connection::syncDone); - QVERIFY(spy2.wait(10000)); - }); - QSignalSpy spy(job, &BaseJob::result); - QVERIFY(spy.wait(10000)); -} - void TestOlmAccount::enableEncryption() { - CREATE_CONNECTION(alice, "alice", "secret", "AlicePhone") - CREATE_CONNECTION(bob, "bob", "secret", "BobPhone") + CREATE_CONNECTION(alice, "alice9", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob4", "secret", "BobPhone") QString joinedRoomId; @@ -547,7 +507,7 @@ void TestOlmAccount::enableEncryption() QVERIFY(spy.wait(10000)); QVERIFY(spy1.wait(10000)); }); - + alice->sync(); QSignalSpy spy(job, &BaseJob::result); QVERIFY(spy.wait(10000)); @@ -558,14 +518,9 @@ void TestOlmAccount::enableEncryption() bool hasEncryption = false; for (auto it = events.rbegin(); it != events.rend(); ++it) { const auto& event = it->event(); - if (eventCast(event)) { + if (is(*event)) { hasEncryption = true; - } else { - qDebug() << event->matrixType() << typeId() - << event->type(); - if (is(*event)) { - qDebug() << event->contentJson(); - } + break; } } QVERIFY(bob->room(joinedRoomId)->usesEncryption()); diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index f1f80454..367092f6 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -28,6 +28,5 @@ private Q_SLOTS: void queryTest(); void claimKeys(); void claimMultipleKeys(); - void keyChange(); void enableEncryption(); }; -- cgit v1.2.3 From 110ca4b01ae86216ee8c03cd2b4eda5ac351df2a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 24 Feb 2022 15:53:15 +0100 Subject: Fix test --- autotests/run-tests.sh | 1 - autotests/testolmaccount.cpp | 52 ++++++++++++++++---------------------------- autotests/testolmaccount.h | 3 +++ 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index 74a8c544..0d58e460 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -28,7 +28,6 @@ echo Register bob docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob1 -p secret -c /data/homeserver.yaml https://localhost:8008' docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob2 -p secret -c /data/homeserver.yaml https://localhost:8008' docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob3 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob4 -p secret -c /data/homeserver.yaml https://localhost:8008' echo Register carl docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 033fff32..04e23db6 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -490,44 +490,30 @@ void TestOlmAccount::claimMultipleKeys() void TestOlmAccount::enableEncryption() { CREATE_CONNECTION(alice, "alice9", "secret", "AlicePhone") - CREATE_CONNECTION(bob, "bob4", "secret", "BobPhone") - QString joinedRoomId; - - auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, - { "@bob:localhost" }); + auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, {}); + bool encryptedEmitted = false; connect(alice.get(), &Connection::newRoom, this, - [alice, bob, &joinedRoomId](Quotient::Room* room) { - room->activateEncryption(); - QSignalSpy spy(room, &Room::encryption); - - joinedRoomId = room->id(); - auto job = bob->joinRoom(room->id()); - QSignalSpy spy1(job, &BaseJob::result); - QVERIFY(spy.wait(10000)); - QVERIFY(spy1.wait(10000)); + [alice, this, &encryptedEmitted](Quotient::Room* room) { + room->activateEncryption(); + connect(room, &Room::encryption, this, [&encryptedEmitted, this](){ + encryptedEmitted = true; + Q_EMIT enableEncryptionFinished(); + }); + connect(alice.get(), &Connection::syncDone, this, [alice, room](){ + if (!room->usesEncryption()) { + alice->sync(); + } }); + alice->sync(); + }); + QSignalSpy createRoomSpy(job, &BaseJob::success); + QVERIFY(createRoomSpy.wait(10000)); alice->sync(); - QSignalSpy spy(job, &BaseJob::result); - QVERIFY(spy.wait(10000)); - bob->sync(); - connect(bob.get(), &Connection::syncDone, this, [bob, joinedRoomId] { - const auto* joinedRoom = bob->room(joinedRoomId); - const auto& events = joinedRoom->messageEvents(); - bool hasEncryption = false; - for (auto it = events.rbegin(); it != events.rend(); ++it) { - const auto& event = it->event(); - if (is(*event)) { - hasEncryption = true; - break; - } - } - QVERIFY(bob->room(joinedRoomId)->usesEncryption()); - QVERIFY(hasEncryption); - }); - QSignalSpy spy2(bob.get(), &Connection::syncDone); - QVERIFY(spy2.wait(10000)); + QSignalSpy finishedSpy(this, &TestOlmAccount::enableEncryptionFinished); + QVERIFY(finishedSpy.wait(10000)); + QVERIFY(encryptedEmitted); } QTEST_GUILESS_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 367092f6..1d3da837 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -13,6 +13,9 @@ class TestOlmAccount : public QObject { Q_OBJECT +Q_SIGNALS: + void enableEncryptionFinished(); + private Q_SLOTS: void pickleUnpickledTest(); void identityKeysValid(); -- cgit v1.2.3 From cfa64f86da6fcfe04947a634a208705543824810 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 24 Feb 2022 17:21:46 +0100 Subject: Fix all tests --- autotests/testolmaccount.cpp | 51 ++++++++++++++++++-------------------------- autotests/testolmaccount.h | 3 --- lib/connection.cpp | 2 +- lib/database.cpp | 4 ++-- lib/database.h | 2 +- 5 files changed, 25 insertions(+), 37 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 04e23db6..9989665a 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -439,6 +439,7 @@ void TestOlmAccount::claimMultipleKeys() QCOMPARE(res->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice->run(res); + QVERIFY(spy.wait(10000)); auto olm1 = alice1->olmAccount(); olm1->generateOneTimeKeys(10); @@ -449,6 +450,7 @@ void TestOlmAccount::claimMultipleKeys() QCOMPARE(res1->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice1->run(res1); + QVERIFY(spy1.wait(10000)); auto olm2 = alice2->olmAccount(); olm2->generateOneTimeKeys(10); @@ -459,10 +461,7 @@ void TestOlmAccount::claimMultipleKeys() QCOMPARE(res2->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice2->run(res2); - - QVERIFY(spy.wait(10000)); - QVERIFY(spy1.wait(10000)); - QVERIFY(spy2.wait(1000)); // TODO this is failing even with 10000 + QVERIFY(spy2.wait(10000)); // Bob will claim all keys from alice CREATE_CONNECTION(bob, "bob3", "secret", "BobPhone") @@ -473,18 +472,17 @@ void TestOlmAccount::claimMultipleKeys() << alice2->deviceId(); QHash> oneTimeKeys; + oneTimeKeys[alice->userId()] = QHash(); for (const auto &d : devices_) { - oneTimeKeys[alice->userId()] = QHash(); oneTimeKeys[alice->userId()][d] = SignedCurve25519Key; } auto job = bob->callApi(oneTimeKeys); - connect(job, &BaseJob::result, this, [bob, job] { - const auto userId = bob->userId(); + QSignalSpy jobSpy(job, &BaseJob::finished); + QVERIFY(jobSpy.wait(10000)); + const auto userId = alice->userId(); - // The device exists. - QCOMPARE(job->oneTimeKeys().size(), 1); - QCOMPARE(job->oneTimeKeys().value(userId).size(), 3); - }); + QCOMPARE(job->oneTimeKeys().size(), 1); + QCOMPARE(job->oneTimeKeys().value(userId).size(), 3); } void TestOlmAccount::enableEncryption() @@ -492,28 +490,21 @@ void TestOlmAccount::enableEncryption() CREATE_CONNECTION(alice, "alice9", "secret", "AlicePhone") auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, {}); - bool encryptedEmitted = false; - connect(alice.get(), &Connection::newRoom, this, - [alice, this, &encryptedEmitted](Quotient::Room* room) { - room->activateEncryption(); - connect(room, &Room::encryption, this, [&encryptedEmitted, this](){ - encryptedEmitted = true; - Q_EMIT enableEncryptionFinished(); - }); - connect(alice.get(), &Connection::syncDone, this, [alice, room](){ - if (!room->usesEncryption()) { - alice->sync(); - } - }); - alice->sync(); - }); QSignalSpy createRoomSpy(job, &BaseJob::success); QVERIFY(createRoomSpy.wait(10000)); alice->sync(); - - QSignalSpy finishedSpy(this, &TestOlmAccount::enableEncryptionFinished); - QVERIFY(finishedSpy.wait(10000)); - QVERIFY(encryptedEmitted); + connect(alice.get(), &Connection::syncDone, this, [alice](){ + qDebug() << "foo"; + alice->sync(); + }); + while(alice->roomsCount(JoinState::Join) == 0) { + QThread::sleep(100); + } + auto room = alice->rooms(JoinState::Join)[0]; + room->activateEncryption(); + QSignalSpy encryptionSpy(room, &Room::encryption); + QVERIFY(encryptionSpy.wait(10000)); + QVERIFY(room->usesEncryption()); } QTEST_GUILESS_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h index 1d3da837..367092f6 100644 --- a/autotests/testolmaccount.h +++ b/autotests/testolmaccount.h @@ -13,9 +13,6 @@ class TestOlmAccount : public QObject { Q_OBJECT -Q_SIGNALS: - void enableEncryptionFinished(); - private Q_SLOTS: void pickleUnpickledTest(); void identityKeysValid(); diff --git a/lib/connection.cpp b/lib/connection.cpp index 1ef2495d..0ef27486 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -579,7 +579,7 @@ void Connection::Private::completeSetup(const QString& mxId) picklingMode = Encrypted { job.binaryData() }; } - database = new Database(data->userId(), q); + database = new Database(data->userId(), data->deviceId(), q); // init olmAccount olmAccount = std::make_unique(data->userId(), data->deviceId(), q); diff --git a/lib/database.cpp b/lib/database.cpp index b91b6ef1..84c93046 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -15,7 +15,7 @@ #include "e2ee/qolminboundsession.h" using namespace Quotient; -Database::Database(const QString& matrixId, QObject* parent) +Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) : QObject(parent) , m_matrixId(matrixId) { @@ -23,7 +23,7 @@ Database::Database(const QString& matrixId, QObject* parent) QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId)); QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId); QDir(databasePath).mkpath(databasePath); - database().setDatabaseName(databasePath + QStringLiteral("/quotient.db3")); + database().setDatabaseName(databasePath + QStringLiteral("/quotient_%1.db3").arg(deviceId)); database().open(); switch(version()) { diff --git a/lib/database.h b/lib/database.h index d244dc0b..d4d5fb56 100644 --- a/lib/database.h +++ b/lib/database.h @@ -14,7 +14,7 @@ class QUOTIENT_API Database : public QObject { Q_OBJECT public: - Database(const QString& matrixId, QObject* parent); + Database(const QString& matrixId, const QString& deviceId, QObject* parent); int version(); void transaction(); -- cgit v1.2.3 From 1458d1fc856c33b25c07ac16a86bbc2096110ba1 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Fri, 25 Feb 2022 15:15:34 +0100 Subject: Don't run e2ee on macos --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f622662b..0bbbc06c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,8 @@ jobs: compiler: GCC - os: windows-2019 e2ee: e2ee # Not supported by the current CI script + - os: macos-10.15 + e2ee: e2ee # Missing OpenSSL include: - os: ubuntu-latest compiler: GCC -- cgit v1.2.3 From 9815e9a0a27f0c4a493ad96e9d865ee489ca9404 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 25 Feb 2022 15:35:07 +0100 Subject: Save key counts to state Otherwise new one time keys will be uploaded on every start --- lib/connection.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0ef27486..4c614176 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -117,6 +117,7 @@ public: bool encryptionUpdateRequired = false; PicklingMode picklingMode = Unencrypted {}; Database *database = nullptr; + QHash oneTimeKeysCount; // A map from SenderKey to vector of InboundSession UnorderedMap> olmSessions; @@ -755,13 +756,12 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { #ifdef Quotient_E2EE_ENABLED - const auto oneTimeKeyCount = - static_cast(data.deviceOneTimeKeysCount()[SignedCurve25519Key]); - if (oneTimeKeyCount < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() + d->oneTimeKeysCount = data.deviceOneTimeKeysCount(); + if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) { d->isUploadingKeys = true; d->olmAccount->generateOneTimeKeys( - d->olmAccount->maxNumberOfOneTimeKeys() / 2 - oneTimeKeyCount); + d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]); auto keys = d->olmAccount->oneTimeKeys(); auto job = d->olmAccount->createUploadKeyRequest(keys); run(job, ForegroundRequest); @@ -1829,6 +1829,10 @@ void Connection::saveState() const QJsonObject { { QStringLiteral("events"), accountDataEvents } }); } + { + QJsonObject keysJson = toJson(d->oneTimeKeysCount); + rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson); + } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = -- cgit v1.2.3 From b0e1455989405ef46eb6d9ed2cd559a1164d04f4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 25 Feb 2022 16:03:59 +0100 Subject: Ifdef --- lib/connection.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4c614176..66df1e43 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1829,10 +1829,12 @@ void Connection::saveState() const QJsonObject { { QStringLiteral("events"), accountDataEvents } }); } +#ifdef Quotient_E2EE_ENABLED { QJsonObject keysJson = toJson(d->oneTimeKeysCount); rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson); } +#endif #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = -- cgit v1.2.3 From 458a0472bd0c6ea0a859c5e55ba5dd3d92ecec99 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 26 Feb 2022 18:55:11 +0100 Subject: Add convenience functions for querying user devices and keys from cache --- lib/connection.cpp | 16 ++++++++++++++++ lib/connection.h | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 66df1e43..ec1c13f5 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2154,4 +2154,20 @@ void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInb { database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), session->pickle(picklingMode())); } + +QStringList Connection::devicesForUser(User* user) const +{ + return d->deviceKeys[user->id()].keys(); +} + +QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const +{ + return d->deviceKeys[user][device].keys[QStringLiteral("curve25519:") + device]; +} + +QString Connection::edKeyForUserDevice(const QString& user, const QString& device) const +{ + return d->deviceKeys[user][device].keys[QStringLiteral("ed25519:") + device]; +} + #endif diff --git a/lib/connection.h b/lib/connection.h index 165d8d68..4a475319 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -680,6 +680,10 @@ public Q_SLOTS: void encryptionUpdate(Room *room); PicklingMode picklingMode() const; QJsonObject decryptNotification(const QJsonObject ¬ification); + + QStringList devicesForUser(User* user) const; + QString curveKeyForUserDevice(const QString &user, const QString& device) const; + QString edKeyForUserDevice(const QString& user, const QString& device) const; #endif Q_SIGNALS: /// \brief Initial server resolution has failed -- cgit v1.2.3 From ccff9edb1c7102cfcc846561a3bdc41fc1496df9 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 26 Feb 2022 18:48:53 +0100 Subject: Handle to-device messages before handling roomdata Probably improves the performance slightly If we handle to room data first, if a message arrives at the same time as the to-device message containing the key and we handle the message first, it will not be decryptable and stored as undecrypted. Then, when the key is handled, the cache of undecrypted messages is searched, the message decrypted and replaced. When handling the key first, the message can be decryped instantly. --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 66df1e43..7099c59c 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -777,11 +777,11 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->consumeDevicesList(data.takeDevicesList()); #endif // Quotient_E2EE_ENABLED + d->consumeToDeviceEvents(data.takeToDeviceEvents()); d->data->setLastEvent(data.nextBatch()); d->consumeRoomData(data.takeRoomData(), fromCache); d->consumeAccountData(data.takeAccountData()); d->consumePresenceData(data.takePresenceData()); - d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED if(d->encryptionUpdateRequired) { d->loadOutdatedUserDevices(); -- cgit v1.2.3 From 29510632780f695eefc9d5107e223aa02caac2c6 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 26 Feb 2022 19:40:13 +0100 Subject: Use QOlmMessage::Type in more places Make sure that the enum values correspond to the values used in the spec and use them instead of magic constants --- lib/connection.cpp | 4 ++-- lib/e2ee/qolmmessage.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 66df1e43..b06c35c2 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -274,10 +274,10 @@ public: QString decrypted; int type = personalCipherObject.value(TypeKeyL).toInt(-1); QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); - if (type == 0) { + if (type == QOlmMessage::PreKey) { QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); decrypted = sessionDecryptPrekey(preKeyMessage, senderKey, account); - } else if (type == 1) { + } else if (type == QOlmMessage::General) { QOlmMessage message(body, QOlmMessage::General); decrypted = sessionDecryptGeneral(message, senderKey); } diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h index 5d5db636..b4285a93 100644 --- a/lib/e2ee/qolmmessage.h +++ b/lib/e2ee/qolmmessage.h @@ -22,8 +22,8 @@ class QUOTIENT_API QOlmMessage : public QByteArray { Q_GADGET public: enum Type { + PreKey = 0, General, - PreKey, }; Q_ENUM(Type) -- 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(-) 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 bb04635c6bc9e051f4bba311aafeb0b0d355f7f5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 27 Feb 2022 08:33:12 +0100 Subject: CI: switch sonar CI config to include E2EE --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bbbc06c..24d58a4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,8 @@ jobs: - os: ubuntu-latest compiler: GCC qt-version: '5.12.12' - sonar: 'sonar' + e2ee: e2ee + sonar: sonar - os: windows-2019 compiler: MSVC platform: x64 -- 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(-) 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 f159d5ec0caf75468c802ee997630af8f7fda02d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 27 Feb 2022 14:11:28 +0100 Subject: Apply suggestions --- lib/connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index ec1c13f5..24951b42 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2162,12 +2162,12 @@ QStringList Connection::devicesForUser(User* user) const QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const { - return d->deviceKeys[user][device].keys[QStringLiteral("curve25519:") + device]; + return d->deviceKeys[user][device].keys["curve25519:" % device]; } QString Connection::edKeyForUserDevice(const QString& user, const QString& device) const { - return d->deviceKeys[user][device].keys[QStringLiteral("ed25519:") + device]; + return d->deviceKeys[user][device].keys["ed25519:" % device]; } #endif -- cgit v1.2.3 From fc2ffecd22d6008426c2305dbeb31fc2caab6163 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 27 Feb 2022 17:43:17 +0100 Subject: Return false instead of error for failed signature checks --- lib/e2ee/qolmutility.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 303f6d75..5d1bc18d 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -49,8 +49,11 @@ std::variant QOlmUtility::ed25519Verify(const QByteArray &key, const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); - const auto error = ret; - if (error == olm_error()) { + if (ret == olm_error()) { + auto error = lastError(m_utility); + if (error == QOlmError::BadMessageMac) { + return false; + } return lastError(m_utility); } -- cgit v1.2.3 From 0928bd405f4958d3a12277614d2715cfac1f7ac0 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 27 Feb 2022 17:47:20 +0100 Subject: Update lib/e2ee/qolmutility.cpp Co-authored-by: Carl Schwan --- lib/e2ee/qolmutility.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 5d1bc18d..9f09a37f 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -54,7 +54,7 @@ std::variant QOlmUtility::ed25519Verify(const QByteArray &key, if (error == QOlmError::BadMessageMac) { return false; } - return lastError(m_utility); + return error; } if (ret != 0) { -- cgit v1.2.3 From bcc8d2d6547e2efd595628a1528ed609eccddad6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 27 Feb 2022 11:02:37 +0100 Subject: Build with shared libs for Sonar Building with static libs fails on QtKeychain apparently underlinking with glib when --coverage is passed. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24d58a4c..b704b3b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,7 @@ jobs: # Build libQuotient as a shared library across platforms but also # check the static configuration somewhere CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DBUILD_SHARED_LIBS=${{ !matrix.sonar && runner.os == 'Linux' }} \ + -DBUILD_SHARED_LIBS=${{ runner.os == 'Linux' }} \ -DCMAKE_INSTALL_PREFIX=~/.local \ -DCMAKE_PREFIX_PATH=~/.local \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" -- 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/connection.cpp | 4 ++-- lib/connection.h | 2 +- lib/database.cpp | 7 ++++--- lib/database.h | 2 +- lib/room.cpp | 6 +++--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4abb77a5..11c81edf 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2150,9 +2150,9 @@ UnorderedMap, QOlmInboundGroupSessionPtr> Connection return database()->loadMegolmSessions(room->id(), picklingMode()); } -void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session) +void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session, const QString& ed25519Key) { - database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), session->pickle(picklingMode())); + database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), ed25519Key, session->pickle(picklingMode())); } QStringList Connection::devicesForUser(User* user) const diff --git a/lib/connection.h b/lib/connection.h index 4a475319..a4986b06 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -318,7 +318,7 @@ public: QOlmAccount* olmAccount() const; Database* database(); UnorderedMap, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); - void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session); + void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session, const QString& ed25519Key); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/database.cpp b/lib/database.cpp index 84c93046..13b41a70 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -83,7 +83,7 @@ void Database::migrateTo1() transaction(); execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, ed25519Key TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); @@ -179,12 +179,13 @@ UnorderedMap, QOlmInboundGroupSessionPtr> Database:: return sessions; } -void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QString& ed25519Key, const QByteArray& pickle) { - auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, pickle) VALUES(:roomId, :senderKey, :sessionId, :pickle);")); + auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, ed25519Key, pickle) VALUES(:roomId, :senderKey, :sessionId, :ed25519Key, :pickle);")); query.bindValue(":roomId", roomId); query.bindValue(":senderKey", senderKey); query.bindValue(":sessionId", sessionId); + query.bindValue(":ed25519Key", ed25519Key); query.bindValue(":pickle", pickle); transaction(); execute(query); diff --git a/lib/database.h b/lib/database.h index d4d5fb56..f6a491fb 100644 --- a/lib/database.h +++ b/lib/database.h @@ -30,7 +30,7 @@ public: void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); - void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle); + void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QString& ed25519Key, const QByteArray& pickle); void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); 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 cc7056851f92ba5b6224b5b82413ec55fd6aaa7f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 7 Mar 2022 20:20:10 +0100 Subject: Guard against device reuse attacks --- lib/connection.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4abb77a5..ae8532c3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1998,6 +1998,7 @@ void Connection::Private::loadOutdatedUserDevices() currentQueryKeysJob = nullptr; const auto data = queryKeysJob->deviceKeys(); for(const auto &[user, keys] : asKeyValueRange(data)) { + QHash oldDevices = deviceKeys[user]; deviceKeys[user].clear(); for(const auto &device : keys) { if(device.userId != user) { @@ -2019,6 +2020,12 @@ void Connection::Private::loadOutdatedUserDevices() "Skipping this device"; continue; } + if (oldDevices.contains(device.deviceId)) { + if (oldDevices[device.deviceId].keys["ed25519:" % device.deviceId] != device.keys["ed25519:" % device.deviceId]) { + qCDebug(E2EE) << "Device reuse detected. Skipping this device"; + continue; + } + } deviceKeys[user][device.deviceId] = device; } outdatedUsers -= user; -- cgit v1.2.3 From ce32dc09a9257ce5cb3188879f549d9b2fa816a1 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 7 Mar 2022 20:56:23 +0100 Subject: Add datbase migration --- lib/database.cpp | 12 +++++++++++- lib/database.h | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/database.cpp b/lib/database.cpp index 13b41a70..d070e1de 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -28,6 +28,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa switch(version()) { case 0: migrateTo1(); + case 1: migrateTo2(); } } @@ -83,7 +84,7 @@ void Database::migrateTo1() transaction(); execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, ed25519Key TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); @@ -94,6 +95,15 @@ void Database::migrateTo1() commit(); } +void Database::migrateTo2() +{ + qCDebug(DATABASE) << "Migrating database to version 2"; + transaction(); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); + execute(QStringLiteral("PRAGMA user_version = 2;")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); diff --git a/lib/database.h b/lib/database.h index f6a491fb..f9a8df1a 100644 --- a/lib/database.h +++ b/lib/database.h @@ -37,6 +37,7 @@ public: private: void migrateTo1(); + void migrateTo2(); QString m_matrixId; }; } -- cgit v1.2.3 From 01f7641292eb5dff236f434d7140259c429521ca Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 22:35:56 +0100 Subject: Store time of last decrypted message for each olm session Is required to correctly choose a session to use for sending messages --- lib/connection.cpp | 4 +++- lib/database.cpp | 16 ++++++++++++++-- lib/database.h | 3 ++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 11c81edf..506a2bc0 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -216,7 +216,7 @@ public: qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get(pickleResult); return; } - q->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult)); + q->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult), QDateTime::currentDateTime()); } QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { @@ -227,6 +227,7 @@ public: qCDebug(E2EE) << "Found inbound session"; const auto result = session->decrypt(message); if(std::holds_alternative(result)) { + q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); return std::get(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; @@ -261,6 +262,7 @@ public: for(auto& session : olmSessions[senderKey]) { const auto result = session->decrypt(message); if(std::holds_alternative(result)) { + q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); return std::get(result); } } diff --git a/lib/database.cpp b/lib/database.cpp index d070e1de..21e79d58 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -100,6 +100,7 @@ void Database::migrateTo2() qCDebug(DATABASE) << "Migrating database to version 2"; transaction(); execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); + execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); execute(QStringLiteral("PRAGMA user_version = 2;")); commit(); } @@ -141,12 +142,13 @@ void Database::clear() } -void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle) +void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp) { - auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle) VALUES(:senderKey, :sessionId, :pickle);")); + auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle, lastReceived) VALUES(:senderKey, :sessionId, :pickle, :lastReceived);")); query.bindValue(":senderKey", senderKey); query.bindValue(":sessionId", sessionId); query.bindValue(":pickle", pickle); + query.bindValue(":lastReceived", timestamp); transaction(); execute(query); commit(); @@ -253,3 +255,13 @@ void Database::clearRoomData(const QString& roomId) execute(query3); commit(); } + +void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET lastReceived=:lastReceived WHERE sessionId=:sessionId;")); + query.bindValue(":lastReceived", timestamp); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} diff --git a/lib/database.h b/lib/database.h index f9a8df1a..cf241dbc 100644 --- a/lib/database.h +++ b/lib/database.h @@ -27,13 +27,14 @@ public: QByteArray accountPickle(); void setAccountPickle(const QByteArray &pickle); void clear(); - void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle); + void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QString& ed25519Key, const QByteArray& pickle); void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); + void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); private: void migrateTo1(); -- cgit v1.2.3 From cbc42d892f7fcc0ef269229c5750266c544729b2 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 9 Mar 2022 20:08:38 +0100 Subject: Update lib/database.cpp Co-authored-by: Carl Schwan --- lib/database.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/database.cpp b/lib/database.cpp index 21e79d58..70dc1b9b 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -101,6 +101,12 @@ void Database::migrateTo2() transaction(); execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); + + // Add indexes for improving queries speed on larger database + execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)")); + execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)")); + execute(QStringLiteral("CREATE INDEX inbound_room_idx ON inbound_megolm_sessions(roomId)")); + execute(QStringLiteral("CREATE INDEX group_session_idx ON group_session_record_index(roomId, sessionId, i)")); execute(QStringLiteral("PRAGMA user_version = 2;")); commit(); } -- cgit v1.2.3 From b8c22ff0663350a019ae918771fac4b6447d2b09 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 9 Mar 2022 20:23:23 +0100 Subject: Check for empty ed25519 key. --- lib/connection.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 506a2bc0..cb04ffe8 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -322,6 +322,11 @@ public: << "in Olm plaintext"; return {}; } + //TODO make this do the check mentioned in the E2EE Implementation guide instead + if (decryptedEvent->fullJson()["keys"]["ed25519"].toString().isEmpty()) { + qCDebug(E2EE) << "Event does not contain an ed25519 key"; + return {}; + } // TODO: keys to constants const auto decryptedEventObject = decryptedEvent->fullJson(); -- cgit v1.2.3 From 4f137493f026afba2299ae5b65ef6eb17939aece Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 27 Feb 2022 19:15:16 +0100 Subject: Add constructor for creating roomkeyevents --- lib/events/roomkeyevent.cpp | 13 +++++++++++++ lib/events/roomkeyevent.h | 1 + 2 files changed, 14 insertions(+) diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 332be3f7..68962950 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -10,3 +10,16 @@ RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) if (roomId().isEmpty()) qCWarning(E2EE) << "Room key event has empty room id"; } + +RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString& sessionId, const QString& sessionKey, const QString& senderId) + : Event(typeId(), { + {"content", QJsonObject{ + {"algorithm", algorithm}, + {"room_id", roomId}, + {"session_id", sessionId}, + {"session_key", sessionKey}, + }}, + {"sender", senderId}, + {"type", "m.room_key"}, + }) +{} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index c4df7936..cb3fe7e7 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -12,6 +12,7 @@ public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) explicit RoomKeyEvent(const QJsonObject& obj); + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId); QString algorithm() const { return contentPart("algorithm"_ls); } QString roomId() const { return contentPart(RoomIdKeyL); } -- 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/connection.cpp | 30 +++++++- lib/connection.h | 9 ++- lib/room.cpp | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 226 insertions(+), 12 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 45888bcb..88c61530 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1306,7 +1306,7 @@ Connection::sendToDevices(const QString& eventType, [&jsonUser](const auto& deviceToEvents) { jsonUser.insert( deviceToEvents.first, - deviceToEvents.second.contentJson()); + deviceToEvents.second->contentJson()); }); }); return callApi(BackgroundRequest, eventType, @@ -2184,4 +2184,32 @@ QString Connection::edKeyForUserDevice(const QString& user, const QString& devic return d->deviceKeys[user][device].keys["ed25519:" % device]; } +bool Connection::hasOlmSession(User* user, const QString& deviceId) const +{ + const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); + return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0; +} + +QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) +{ + //TODO be smarter about choosing a session; see e2ee impl guide + //TODO create session? + const auto& curveKey = curveKeyForUserDevice(user->id(), device); + QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); + auto result = d->olmSessions[curveKey][0]->encrypt(message); + return qMakePair(type, result.toCiphertext()); +} + +//TODO be more consistent with curveKey and identityKey +void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) +{ + auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); + if (std::holds_alternative(session)) { + //TODO something + qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + } + d->saveSession(std::get>(session), theirIdentityKey); + d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index a4986b06..5ea7c5f1 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -24,6 +24,7 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -132,7 +133,7 @@ class QUOTIENT_API Connection : public QObject { public: using UsersToDevicesToEvents = - UnorderedMap>; + UnorderedMap>>; enum RoomVisibility { PublishRoom, @@ -319,6 +320,12 @@ public: Database* database(); UnorderedMap, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session, const QString& ed25519Key); + bool hasOlmSession(User* user, const QString& deviceId) const; + + //This currently assumes that an olm session with (user, device) exists + //TODO make this return an event? + QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); + void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; 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/connection.cpp | 11 ++++++++++- lib/connection.h | 5 +++++ lib/database.cpp | 41 +++++++++++++++++++++++++++++++++++++++- lib/database.h | 3 +++ lib/e2ee/qolmoutboundsession.cpp | 24 +++++++++++++++++++++-- lib/e2ee/qolmoutboundsession.h | 10 +++++++++- lib/room.cpp | 33 +++++++++++++++----------------- 7 files changed, 104 insertions(+), 23 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 88c61530..d01b8ac4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2193,7 +2193,6 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { //TODO be smarter about choosing a session; see e2ee impl guide - //TODO create session? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); @@ -2212,4 +2211,14 @@ void Connection::createOlmSession(const QString& theirIdentityKey, const QString d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); } +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) +{ + return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode); +} + +void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data) +{ + d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index 5ea7c5f1..6f75b068 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -25,6 +25,7 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" +#include "e2ee/qolmoutboundsession.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -322,6 +323,10 @@ public: void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session, const QString& ed25519Key); bool hasOlmSession(User* user, const QString& deviceId) const; + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); + void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); + + //This currently assumes that an olm session with (user, device) exists //TODO make this return an event? QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); diff --git a/lib/database.cpp b/lib/database.cpp index 70dc1b9b..ad653073 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -85,7 +85,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, sessionId TEXT, pickle TEXT, creationTime TEXT, messageCount INTEGER);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); @@ -271,3 +271,42 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi execute(query); commit(); } + +void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) +{ + const auto pickle = session->pickle(picklingMode); + if (std::holds_alternative(pickle)) { + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); + deleteQuery.bindValue(":roomId", roomId); + deleteQuery.bindValue(":sessionId", session->sessionId()); + + auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); + insertQuery.bindValue(":roomId", roomId); + insertQuery.bindValue(":sessionId", session->sessionId()); + insertQuery.bindValue(":pickle", std::get(pickle)); + insertQuery.bindValue(":creationTime", session->creationTime()); + insertQuery.bindValue(":messageCount", session->messageCount()); + + transaction(); + execute(deleteQuery); + execute(insertQuery); + commit(); + } +} + +QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;")); + query.bindValue(":roomId", roomId); + execute(query); + if (query.next()) { + auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative(sessionResult)) { + auto session = std::move(std::get(sessionResult)); + session->setCreationTime(query.value("creationTime").toDateTime()); + session->setMessageCount(query.value("messageCount").toInt()); + return session; + } + } + return nullptr; +} diff --git a/lib/database.h b/lib/database.h index cf241dbc..dccfb770 100644 --- a/lib/database.h +++ b/lib/database.h @@ -8,6 +8,7 @@ #include #include "e2ee/e2ee.h" +#include "e2ee/qolmoutboundsession.h" namespace Quotient { class QUOTIENT_API Database : public QObject @@ -35,6 +36,8 @@ public: std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); + void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); private: void migrateTo1(); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index da32417b..8852bcf3 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -61,13 +61,13 @@ std::variant QOlmOutboundGroupSession::pickle(const Pickl return pickledBuf; } -std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); QByteArray key = toKey(mode); const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), - pickled.data(), pickled.length()); + pickledBuf.data(), pickledBuf.length()); if (error == olm_error()) { return lastError(olmOutboundGroupSession); } @@ -123,3 +123,23 @@ std::variant QOlmOutboundGroupSession::sessionKey() const } return keyBuffer; } + +int QOlmOutboundGroupSession::messageCount() const +{ + return m_messageCount; +} + +void QOlmOutboundGroupSession::setMessageCount(int messageCount) +{ + m_messageCount = messageCount; +} + +QDateTime QOlmOutboundGroupSession::creationTime() const +{ + return m_creationTime; +} + +void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime) +{ + m_creationTime = creationTime; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 32ba2b3b..10ca35c0 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -25,7 +25,7 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. static std::variant, QOlmError> - unpickle(QByteArray& pickled, const PicklingMode& mode); + unpickle(const QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. std::variant encrypt(const QString &plaintext); @@ -44,8 +44,16 @@ public: //! ratchet key that will be used for the next message. std::variant sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); + + int messageCount() const; + void setMessageCount(int messageCount); + + QDateTime creationTime() const; + void setCreationTime(const QDateTime& creationTime); private: OlmOutboundGroupSession *m_groupSession; + int m_messageCount = 0; + QDateTime m_creationTime = QDateTime::currentDateTime(); }; using QOlmOutboundGroupSessionPtr = std::unique_ptr; 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/connection.cpp | 1 + lib/database.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ lib/database.h | 9 +++++++++ lib/room.cpp | 53 ++++++++++++++++++++++++++++++++++------------------- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d01b8ac4..691f4ab3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2193,6 +2193,7 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { //TODO be smarter about choosing a session; see e2ee impl guide + //TODO do we need to save the olm session after sending a message? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); diff --git a/lib/database.cpp b/lib/database.cpp index ad653073..863cbf0d 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -13,6 +13,9 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" +#include "connection.h" +#include "user.h" +#include "room.h" using namespace Quotient; Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) @@ -90,6 +93,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -310,3 +314,43 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt } return nullptr; } + +void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) +{ + //TODO this better + auto connection = dynamic_cast(parent()); + transaction(); + for (const auto& user : devices.keys()) { + for (const auto& device : devices[user]) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user->id()); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device)); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); + } + } + commit(); +} + +QHash Database::devicesWithoutKey(Room* room, const QString &sessionId) +{ + auto connection = dynamic_cast(parent()); + QHash devices; + for (const auto& user : room->users()) {//TODO does this include invited & left? + devices[user->id()] = connection->devicesForUser(user); + } + + auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); + query.bindValue(":roomId", room->id()); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); + while (query.next()) { + devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString()); + } + return devices; +} diff --git a/lib/database.h b/lib/database.h index dccfb770..ed567a86 100644 --- a/lib/database.h +++ b/lib/database.h @@ -7,10 +7,15 @@ #include #include +#include + #include "e2ee/e2ee.h" #include "e2ee/qolmoutboundsession.h" namespace Quotient { +class User; +class Room; + class QUOTIENT_API Database : public QObject { Q_OBJECT @@ -39,6 +44,10 @@ public: QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); + // Returns a map User -> [Device] that have not received key yet + QHash devicesWithoutKey(Room* room, const QString &sessionId); + void setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index); + private: void migrateTo1(); void migrateTo2(); 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 --- autotests/CMakeLists.txt | 1 + autotests/testfilecrypto.cpp | 17 +++++++++++ autotests/testfilecrypto.h | 12 ++++++++ lib/eventitem.cpp | 10 +++++++ lib/eventitem.h | 3 ++ lib/events/encryptedfile.cpp | 26 +++++++++++++++-- lib/events/encryptedfile.h | 1 + lib/room.cpp | 67 +++++++++++++++++++++++++++++--------------- lib/room.h | 2 +- 9 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 autotests/testfilecrypto.cpp create mode 100644 autotests/testfilecrypto.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 671d6c08..c11901bf 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -18,4 +18,5 @@ if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testgroupsession) quotient_add_test(NAME testolmsession) quotient_add_test(NAME testolmutility) + quotient_add_test(NAME testfilecrypto) endif() diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp new file mode 100644 index 00000000..e6bec1fe --- /dev/null +++ b/autotests/testfilecrypto.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testfilecrypto.h" +#include "events/encryptedfile.h" +#include + +using namespace Quotient; +void TestFileCrypto::encryptDecryptData() +{ + QByteArray data = "ABCDEF"; + auto [file, cipherText] = EncryptedFile::encryptFile(data); + auto decrypted = file.decryptFile(cipherText); + QCOMPARE(data, decrypted); +} +QTEST_APPLESS_MAIN(TestFileCrypto) diff --git a/autotests/testfilecrypto.h b/autotests/testfilecrypto.h new file mode 100644 index 00000000..9096a8c7 --- /dev/null +++ b/autotests/testfilecrypto.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +class TestFileCrypto : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void encryptDecryptData(); +}; diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index a2d65d8d..302ae053 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -26,6 +26,16 @@ void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) setStatus(EventStatus::FileUploaded); } +void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) +{ + if (auto* rme = getAs()) { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([encryptedFile](EventContent::TypedBase& ec) { + ec.fileInfo()->file = encryptedFile; + }); + } +} + // Not exactly sure why but this helps with the linker not finding // Quotient::EventStatus::staticMetaObject when building Quaternion #include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index f04ef323..d8313736 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -9,6 +9,8 @@ #include #include +#include "events/encryptedfile.h" + namespace Quotient { namespace EventStatus { @@ -114,6 +116,7 @@ public: void setDeparted() { setStatus(EventStatus::Departed); } void setFileUploaded(const QUrl& remoteUrl); + void setEncryptedFile(const EncryptedFile& encryptedFile); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index d4a517bd..e90be428 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -8,6 +8,7 @@ #ifdef Quotient_E2EE_ENABLED #include #include +#include "e2ee/qolmutils.h" #endif using namespace Quotient; @@ -27,7 +28,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const { int length; auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_CIPHER_CTX_block_size(ctx) + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, @@ -44,7 +45,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const + length, &length); EVP_CIPHER_CTX_free(ctx); - return plaintext; + return plaintext.left(ciphertext.size()); } #else qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " @@ -53,6 +54,27 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const #endif } +std::pair EncryptedFile::encryptFile(const QByteArray &plainText) +{ + QByteArray k = getRandom(32); + auto kBase64 = k.toBase64(); + QByteArray iv = getRandom(16); + JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true}; + + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); + EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); + EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256).toBase64(); + auto ivBase64 = iv.toBase64(); + EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; + return {file, cipherText}; +} + void JsonObjectConverter::dumpTo(QJsonObject& jo, const EncryptedFile& pod) { diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 0558563f..76aff837 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,6 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; + static std::pair encryptFile(const QByteArray &plainText); }; template <> 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())); diff --git a/lib/room.h b/lib/room.h index 9f70d77a..1a1d839f 100644 --- a/lib/room.h +++ b/lib/room.h @@ -997,7 +997,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = std::nullopt); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- 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/events/encryptedevent.cpp | 7 +++++++ lib/events/encryptedevent.h | 2 ++ lib/room.cpp | 3 +++ 3 files changed, 12 insertions(+) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 9d07a35f..3af3d6ff 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -61,3 +61,10 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const } return loadEvent(eventObject); } + +void EncryptedEvent::setRelation(const QJsonObject& relation) +{ + auto content = editJson()["content"_ls].toObject(); + content["m.relates_to"] = relation; + editJson()["content"] = content; +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 72efffd4..ddd5e415 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -56,6 +56,8 @@ public: QString deviceId() const { return contentPart(DeviceIdKeyL); } QString sessionId() const { return contentPart(SessionIdKeyL); } RoomEventPtr createDecrypted(const QString &decrypted) const; + + void setRelation(const QJsonObject& relation); }; REGISTER_EVENT_TYPE(EncryptedEvent) 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/connection.cpp | 38 +++++++++++++++++++++++++++++++------- lib/connection.h | 3 +-- lib/database.cpp | 18 +++++++++++++++--- lib/database.h | 3 ++- lib/events/encryptedfile.cpp | 4 ++++ lib/events/roomevent.h | 8 ++++++++ lib/room.cpp | 27 ++++++++++++++++----------- 7 files changed, 77 insertions(+), 24 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 691f4ab3..0ef002ca 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -33,6 +33,7 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#include #ifdef Quotient_E2EE_ENABLED # include "e2ee/qolmaccount.h" @@ -221,13 +222,23 @@ public: QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { Q_ASSERT(message.type() == QOlmMessage::PreKey); - for(auto& session : olmSessions[senderKey]) { + for (size_t i = 0; i < olmSessions[senderKey].size(); i++) { + auto& session = olmSessions[senderKey][i]; const auto matches = session->matchesInboundSessionFrom(senderKey, message); if(std::holds_alternative(matches) && std::get(matches)) { qCDebug(E2EE) << "Found inbound session"; const auto result = session->decrypt(message); if(std::holds_alternative(result)) { q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); + auto pickle = session->pickle(q->picklingMode()); + if (std::holds_alternative(pickle)) { + q->database()->updateOlmSession(senderKey, session->sessionId(), std::get(pickle)); + } else { + qCWarning(E2EE) << "Failed to pickle olm session."; + } + auto s = std::move(session); + olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i); + olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s)); return std::get(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; @@ -248,7 +259,7 @@ public: } const auto result = newSession->decrypt(message); saveSession(newSession, senderKey); - olmSessions[senderKey].push_back(std::move(newSession)); + olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(newSession)); if(std::holds_alternative(result)) { return std::get(result); } else { @@ -259,10 +270,20 @@ public: QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) { Q_ASSERT(message.type() == QOlmMessage::General); - for(auto& session : olmSessions[senderKey]) { + for (size_t i = 0; i < olmSessions[senderKey].size(); i++) { + auto& session = olmSessions[senderKey][i]; const auto result = session->decrypt(message); if(std::holds_alternative(result)) { q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); + auto pickle = session->pickle(q->picklingMode()); + if (std::holds_alternative(pickle)) { + q->database()->updateOlmSession(senderKey, session->sessionId(), std::get(pickle)); + } else { + qCWarning(E2EE) << "Failed to pickle olm session."; + } + auto s = std::move(session); + olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i); + olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s)); return std::get(result); } } @@ -2192,21 +2213,24 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { - //TODO be smarter about choosing a session; see e2ee impl guide - //TODO do we need to save the olm session after sending a message? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); + auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); + if (std::holds_alternative(pickle)) { + database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get(pickle)); + } else { + qCWarning(E2EE) << "Failed to pickle olm session."; + } return qMakePair(type, result.toCiphertext()); } -//TODO be more consistent with curveKey and identityKey void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) { auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); if (std::holds_alternative(session)) { - //TODO something qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + return; } d->saveSession(std::get>(session), theirIdentityKey); d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); diff --git a/lib/connection.h b/lib/connection.h index 6f75b068..52e1700c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -327,8 +327,7 @@ public: void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); - //This currently assumes that an olm session with (user, device) exists - //TODO make this return an event? + //This assumes that an olm session with (user, device) exists QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED diff --git a/lib/database.cpp b/lib/database.cpp index 863cbf0d..d9dce517 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" @@ -166,7 +167,7 @@ void Database::saveOlmSession(const QString& senderKey, const QString& sessionId UnorderedMap> Database::loadOlmSessions(const PicklingMode& picklingMode) { - auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions ORDER BY lastReceived DESC;")); transaction(); execute(query); commit(); @@ -317,7 +318,6 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) { - //TODO this better auto connection = dynamic_cast(parent()); transaction(); for (const auto& user : devices.keys()) { @@ -339,7 +339,7 @@ QHash Database::devicesWithoutKey(Room* room, const QStrin { auto connection = dynamic_cast(parent()); QHash devices; - for (const auto& user : room->users()) {//TODO does this include invited & left? + for (const auto& user : room->users()) { devices[user->id()] = connection->devicesForUser(user); } @@ -354,3 +354,15 @@ QHash Database::devicesWithoutKey(Room* room, const QStrin } return devices; } + +void Database::updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;")); + query.bindValue(":pickle", pickle); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + diff --git a/lib/database.h b/lib/database.h index ed567a86..76d86f12 100644 --- a/lib/database.h +++ b/lib/database.h @@ -33,7 +33,7 @@ public: QByteArray accountPickle(); void setAccountPickle(const QByteArray &pickle); void clear(); - void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp); + void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle, const QDateTime& timestamp); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QString& ed25519Key, const QByteArray& pickle); @@ -43,6 +43,7 @@ public: void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); + void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); // Returns a map User -> [Device] that have not received key yet QHash devicesWithoutKey(Room* room, const QString &sessionId); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index e90be428..bb4e26c7 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -56,6 +56,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const std::pair EncryptedFile::encryptFile(const QByteArray &plainText) { +#ifdef Quotient_E2EE_ENABLED QByteArray k = getRandom(32); auto kBase64 = k.toBase64(); QByteArray iv = getRandom(16); @@ -73,6 +74,9 @@ std::pair EncryptedFile::encryptFile(const QByteArray auto ivBase64 = iv.toBase64(); EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; return {file, cipherText}; +#else + return {{}, {}}; +#endif } void JsonObjectConverter::dumpTo(QJsonObject& jo, diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index c4b0131a..a7d6c428 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -80,6 +80,14 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = Range; +template <> +inline EventPtr doLoadEvent(const QJsonObject& json, const QString& matrixType) +{ + if (matrixType == "m.room.encrypted") + return RoomEvent::factory.loadEvent(json, matrixType); + return Event::factory.loadEvent(json, matrixType); +} + class QUOTIENT_API CallEventBase : public RoomEvent { public: CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, 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 80251f1db42268be0e678543b0b054f925fbfe88 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 10 Mar 2022 21:47:51 +0100 Subject: Update lib/events/encryptedfile.h --- lib/events/encryptedfile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 76aff837..b2808395 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,7 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; - static std::pair encryptFile(const QByteArray &plainText); + static std::pair encryptFile(const QByteArray& plainText); }; template <> -- 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(-) 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(-) 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 494099a4b51b4d537a40a89c9d642fe3bd61e559 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 11 Mar 2022 15:05:39 +0100 Subject: Fix loading images when E2EE is disabled --- lib/mxcreply.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 1d40c5e1..b757bb93 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -54,11 +54,11 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) buffer->open(ReadOnly); d->m_device = buffer; } - setOpenMode(ReadOnly); - emit finished(); #else d->m_device = d->m_reply; #endif + setOpenMode(ReadOnly); + emit finished(); }); #ifdef Quotient_E2EE_ENABLED -- cgit v1.2.3 From 7cd71c978f39a0bd8f82ebdf01cbaaf317ebe020 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 23 Mar 2022 20:42:28 +0100 Subject: Add database migration --- lib/database.cpp | 15 ++++++++++++--- lib/database.h | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index d9dce517..b0e3b415 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -33,6 +33,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa switch(version()) { case 0: migrateTo1(); case 1: migrateTo2(); + case 2: migrateTo3(); } } @@ -89,12 +90,11 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, sessionId TEXT, pickle TEXT, creationTime TEXT, messageCount INTEGER);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); - execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -106,7 +106,7 @@ void Database::migrateTo2() transaction(); execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); - + // Add indexes for improving queries speed on larger database execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)")); execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)")); @@ -116,6 +116,15 @@ void Database::migrateTo2() commit(); } +void Database::migrateTo3() +{ + qCDebug(DATABASE) << "Migrating database to version 3"; + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); + execute(QStringLiteral("PRAGMA user_version = 3;")); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); diff --git a/lib/database.h b/lib/database.h index 76d86f12..4a047161 100644 --- a/lib/database.h +++ b/lib/database.h @@ -52,6 +52,7 @@ public: private: void migrateTo1(); void migrateTo2(); + void migrateTo3(); QString m_matrixId; }; } -- cgit v1.2.3 From fc3ad90a054e3c674127a0cdd385ddbb98cf2010 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 8 Apr 2022 23:23:30 +0200 Subject: Correctly load EncryptedEvents --- lib/events/roomevent.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index c4b0131a..a7d6c428 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -80,6 +80,14 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = Range; +template <> +inline EventPtr doLoadEvent(const QJsonObject& json, const QString& matrixType) +{ + if (matrixType == "m.room.encrypted") + return RoomEvent::factory.loadEvent(json, matrixType); + return Event::factory.loadEvent(json, matrixType); +} + class QUOTIENT_API CallEventBase : public RoomEvent { public: CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, -- cgit v1.2.3 From 42abb01516ee4d3d0fe11ffddd47c7e76d786385 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 4 Apr 2022 17:28:52 +0200 Subject: Check edKey when receiving an olm message --- lib/connection.cpp | 14 +++++++++++--- lib/database.cpp | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 45888bcb..1250eddf 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -322,9 +322,17 @@ public: << "in Olm plaintext"; return {}; } - //TODO make this do the check mentioned in the E2EE Implementation guide instead - if (decryptedEvent->fullJson()["keys"]["ed25519"].toString().isEmpty()) { - qCDebug(E2EE) << "Event does not contain an ed25519 key"; + + auto query = database->prepareQuery(QStringLiteral("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;")); + query.bindValue(":curveKey", encryptedEvent.contentJson()["sender_key"].toString()); + database->execute(query); + if (!query.next()) { + qCWarning(E2EE) << "Received olm message from unknown device" << encryptedEvent.contentJson()["sender_key"].toString(); + return {}; + } + auto edKey = decryptedEvent->fullJson()["keys"]["ed25519"].toString(); + if (edKey.isEmpty() || query.value(QStringLiteral("edKey")).toString() != edKey) { + qCDebug(E2EE) << "Received olm message with invalid ed key"; return {}; } diff --git a/lib/database.cpp b/lib/database.cpp index 70dc1b9b..d719d027 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -99,6 +99,7 @@ void Database::migrateTo2() { qCDebug(DATABASE) << "Migrating database to version 2"; transaction(); + //TODO remove this column again - we don't need it after all execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); -- cgit v1.2.3 From 5c93193508a49b79a92fd0d80cf4db14f1d0762e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 8 Apr 2022 23:23:07 +0200 Subject: Make sure devices are known before decrypting olm messages --- lib/connection.cpp | 68 ++++++++++++++++++++++++++++++++++++++++-------------- lib/connection.h | 2 ++ 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 1250eddf..d7460f08 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -118,6 +118,8 @@ public: PicklingMode picklingMode = Unencrypted {}; Database *database = nullptr; QHash oneTimeKeysCount; + std::vector> pendingEncryptedEvents; + void handleEncryptedToDeviceEvent(const EncryptedEvent& event); // A map from SenderKey to vector of InboundSession UnorderedMap> olmSessions; @@ -937,30 +939,44 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); return; } - const auto decryptedEvent = sessionDecryptMessage(event); - if(!decryptedEvent) { - qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + if (q->isKnownCurveKey(event.senderId(), event.senderKey())) { + handleEncryptedToDeviceEvent(event); return; } - - switchOnType(*decryptedEvent, - [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); - } else { - qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - } - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); - }); + trackedUsers += event.senderId(); + outdatedUsers += event.senderId(); + encryptionUpdateRequired = true; + pendingEncryptedEvents.push_back(std::make_unique(event.fullJson())); + }, [](const Event& e){ + // Unhandled }); } #endif } +void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) +{ + const auto decryptedEvent = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } + + switchOnType(*decryptedEvent, + [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); + } else { + qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { + qCDebug(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); +} + void Connection::Private::consumeDevicesList(DevicesList&& devicesList) { #ifdef Quotient_E2EE_ENABLED @@ -2046,6 +2062,15 @@ void Connection::Private::loadOutdatedUserDevices() outdatedUsers -= user; } saveDevicesList(); + + for(size_t i = 0; i < pendingEncryptedEvents.size();) { + if (q->isKnownCurveKey(pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), pendingEncryptedEvents[i]->contentJson()["sender_key"].toString())) { + handleEncryptedToDeviceEvent(*(pendingEncryptedEvents[i].get())); + pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i); + } else { + i++; + } + } }); } @@ -2192,4 +2217,13 @@ QString Connection::edKeyForUserDevice(const QString& user, const QString& devic return d->deviceKeys[user][device].keys["ed25519:" % device]; } +bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) +{ + auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey")); + query.bindValue(":matrixId", user); + query.bindValue(":curveKey", curveKey); + database()->execute(query); + return query.next(); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index a4986b06..9f23902b 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -684,6 +684,7 @@ public Q_SLOTS: QStringList devicesForUser(User* user) const; QString curveKeyForUserDevice(const QString &user, const QString& device) const; QString edKeyForUserDevice(const QString& user, const QString& device) const; + bool isKnownCurveKey(const QString& user, const QString& curveKey); #endif Q_SIGNALS: /// \brief Initial server resolution has failed @@ -841,6 +842,7 @@ Q_SIGNALS: void cacheStateChanged(); void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); + void devicesListLoaded(); protected: /** -- 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(-) 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/connection.cpp | 42 +++++++++++++++++++++++----------------- lib/connection.h | 4 ++-- lib/database.cpp | 32 +++++++++++++++++++++++------- lib/database.h | 7 ++++--- lib/e2ee/qolminboundsession.cpp | 18 +++++++++++++++++ lib/e2ee/qolminboundsession.h | 11 +++++++++++ lib/room.cpp | 43 +++++++++++++++++++++++------------------ lib/room.h | 2 +- 8 files changed, 109 insertions(+), 50 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d7460f08..0bdc0489 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -220,7 +220,7 @@ public: } q->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult), QDateTime::currentDateTime()); } - QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) + std::pair sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) { Q_ASSERT(message.type() == QOlmMessage::PreKey); for(auto& session : olmSessions[senderKey]) { @@ -230,7 +230,7 @@ public: const auto result = session->decrypt(message); if(std::holds_alternative(result)) { q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); - return std::get(result); + return { std::get(result), session->sessionId() }; } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; return {}; @@ -249,47 +249,53 @@ public: qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); } const auto result = newSession->decrypt(message); + QString sessionId = newSession->sessionId(); saveSession(newSession, senderKey); olmSessions[senderKey].push_back(std::move(newSession)); if(std::holds_alternative(result)) { - return std::get(result); + return { std::get(result), sessionId }; } else { qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; return {}; } } - QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) + std::pair sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) { Q_ASSERT(message.type() == QOlmMessage::General); for(auto& session : olmSessions[senderKey]) { const auto result = session->decrypt(message); if(std::holds_alternative(result)) { q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); - return std::get(result); + return { std::get(result), session->sessionId() }; } } qCWarning(E2EE) << "Failed to decrypt message"; return {}; } - QString sessionDecryptMessage( + std::pair sessionDecryptMessage( const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account) { QString decrypted; + QString olmSessionId; int type = personalCipherObject.value(TypeKeyL).toInt(-1); QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); if (type == QOlmMessage::PreKey) { QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); - decrypted = sessionDecryptPrekey(preKeyMessage, senderKey, account); + auto result = sessionDecryptPrekey(preKeyMessage, senderKey, account); + decrypted = result.first; + olmSessionId = result.second; } else if (type == QOlmMessage::General) { QOlmMessage message(body, QOlmMessage::General); - decrypted = sessionDecryptGeneral(message, senderKey); + auto result = sessionDecryptGeneral(message, senderKey); + decrypted = result.first; + olmSessionId = result.second; } - return decrypted; + return { decrypted, olmSessionId }; } #endif - EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) + std::pair sessionDecryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; @@ -305,7 +311,7 @@ public: qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } - const auto decrypted = sessionDecryptMessage( + const auto [decrypted, olmSessionId] = sessionDecryptMessage( personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" @@ -356,7 +362,7 @@ public: return {}; } - return std::move(decryptedEvent); + return { std::move(decryptedEvent), olmSessionId }; #endif // Quotient_E2EE_ENABLED } #ifdef Quotient_E2EE_ENABLED @@ -956,16 +962,16 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) { - const auto decryptedEvent = sessionDecryptMessage(event); + const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); if(!decryptedEvent) { qCWarning(E2EE) << "Failed to decrypt event" << event.id(); return; } switchOnType(*decryptedEvent, - [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { + [this, senderKey = event.senderKey(), &event, olmSessionId](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); } else { qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); @@ -2192,14 +2198,14 @@ Database* Connection::database() return d->database; } -UnorderedMap, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room) +UnorderedMap Connection::loadRoomMegolmSessions(Room* room) { return database()->loadMegolmSessions(room->id(), picklingMode()); } -void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session, const QString& ed25519Key) +void Connection::saveMegolmSession(Room* room, QOlmInboundGroupSession* session) { - database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), ed25519Key, session->pickle(picklingMode())); + database()->saveMegolmSession(room->id(), session->sessionId(), session->pickle(picklingMode()), session->senderId(), session->olmSessionId()); } QStringList Connection::devicesForUser(User* user) const diff --git a/lib/connection.h b/lib/connection.h index 9f23902b..29731593 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -317,8 +317,8 @@ public: #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; Database* database(); - UnorderedMap, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); - void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session, const QString& ed25519Key); + UnorderedMap loadRoomMegolmSessions(Room* room); + void saveMegolmSession(Room* room, QOlmInboundGroupSession* session); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/database.cpp b/lib/database.cpp index d719d027..3189b3f1 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -29,6 +29,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa switch(version()) { case 0: migrateTo1(); case 1: migrateTo2(); + case 2: migrateTo3(); } } @@ -112,6 +113,20 @@ void Database::migrateTo2() commit(); } +void Database::migrateTo3() +{ + qCDebug(DATABASE) << "Migrating database to version 3"; + transaction(); + + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions_temp AS SELECT roomId, sessionId, pickle FROM inbound_megolm_sessions;")); + execute(QStringLiteral("DROP TABLE inbound_megolm_sessions;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions_temp RENAME TO inbound_megolm_sessions;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD olmSessionId TEXT;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD senderId TEXT;")); + execute(QStringLiteral("PRAGMA user_version = 3;")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); @@ -179,33 +194,36 @@ UnorderedMap> Database::loadOlmSessions(con return sessions; } -UnorderedMap, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) +UnorderedMap Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) { auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;")); query.bindValue(":roomId", roomId); transaction(); execute(query); commit(); - UnorderedMap, QOlmInboundGroupSessionPtr> sessions; + UnorderedMap sessions; while (query.next()) { auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); if (std::holds_alternative(session)) { qCWarning(E2EE) << "Failed to unpickle megolm session"; continue; } - sessions[{query.value("senderKey").toString(), query.value("sessionId").toString()}] = std::move(std::get(session)); + + sessions[query.value("sessionId").toString()] = std::move(std::get(session)); + sessions[query.value("sessionId").toString()]->setOlmSessionId(query.value("olmSessionId").toString()); + sessions[query.value("sessionId").toString()]->setSenderId(query.value("senderId").toString()); } return sessions; } -void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QString& ed25519Key, const QByteArray& pickle) +void Database::saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId) { - auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, ed25519Key, pickle) VALUES(:roomId, :senderKey, :sessionId, :ed25519Key, :pickle);")); + auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId);")); query.bindValue(":roomId", roomId); - query.bindValue(":senderKey", senderKey); query.bindValue(":sessionId", sessionId); - query.bindValue(":ed25519Key", ed25519Key); query.bindValue(":pickle", pickle); + query.bindValue(":senderId", senderId); + query.bindValue(":olmSessionId", olmSessionId); transaction(); execute(query); commit(); diff --git a/lib/database.h b/lib/database.h index cf241dbc..08fe49f3 100644 --- a/lib/database.h +++ b/lib/database.h @@ -8,7 +8,6 @@ #include #include "e2ee/e2ee.h" - namespace Quotient { class QUOTIENT_API Database : public QObject { @@ -29,8 +28,8 @@ public: void clear(); void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); - UnorderedMap, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); - void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QString& ed25519Key, const QByteArray& pickle); + UnorderedMap loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId); void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); @@ -39,6 +38,8 @@ public: private: void migrateTo1(); void migrateTo2(); + void migrateTo3(); + QString m_matrixId; }; } diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 2e9cc716..60d871ef 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -149,3 +149,21 @@ bool QOlmInboundGroupSession::isVerified() const { return olm_inbound_group_session_is_verified(m_groupSession) != 0; } + +QString QOlmInboundGroupSession::olmSessionId() const +{ + return m_olmSessionId; +} +void QOlmInboundGroupSession::setOlmSessionId(const QString& olmSessionId) +{ + m_olmSessionId = olmSessionId; +} + +QString QOlmInboundGroupSession::senderId() const +{ + return m_senderId; +} +void QOlmInboundGroupSession::setSenderId(const QString& senderId) +{ + m_senderId = senderId; +} diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 437f753d..32112b97 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -41,9 +41,20 @@ public: QByteArray sessionId() const; bool isVerified() const; + //! The olm session that this session was received from. + //! Required to get the device this session is from. + QString olmSessionId() const; + void setOlmSessionId(const QString& setOlmSessionId); + + //! The sender of this session. + QString senderId() const; + void setSenderId(const QString& senderId); + QOlmInboundGroupSession(OlmInboundGroupSession* session); private: OlmInboundGroupSession* m_groupSession; + QString m_olmSessionId; + QString m_senderId; }; using QOlmInboundGroupSessionPtr = std::unique_ptr; 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()]; diff --git a/lib/room.h b/lib/room.h index 9f70d77a..6ba7feac 100644 --- a/lib/room.h +++ b/lib/room.h @@ -277,7 +277,7 @@ public: int timelineSize() const; bool usesEncryption() const; RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); - void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderKey); + void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderId, const QString& olmSessionId); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; -- 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(-) 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 ad50a5167b98579e8e4960352653dca995490368 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Apr 2022 20:47:53 +0200 Subject: Try fixing lgtm.com --- lib/connection.cpp | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0bdc0489..4ca82888 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -962,25 +962,28 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) { - const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); - if(!decryptedEvent) { - qCWarning(E2EE) << "Failed to decrypt event" << event.id(); - return; - } + const auto [decryptedEvent, id] = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } - switchOnType(*decryptedEvent, - [this, senderKey = event.senderKey(), &event, olmSessionId](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); - } else { - qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - } - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); - }); + // Yes, this is weird, but lgtm.com doesn't like it otherwise + const auto olmSessionId = id; + + switchOnType(*decryptedEvent, + [this, senderKey = event.senderKey(), &event, olmSessionId](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); + } else { + qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { + qCDebug(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); } void Connection::Private::consumeDevicesList(DevicesList&& devicesList) -- cgit v1.2.3 From 06b7093aaa0b5cce58c1191c7e0bf9bdfe135005 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Apr 2022 20:53:58 +0200 Subject: Only build function when E2EE is enabled --- lib/connection.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4ca82888..1a5e3c6e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -960,6 +960,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) #endif } +#ifdef Quotient_E2EE_ENABLED void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) { const auto [decryptedEvent, id] = sessionDecryptMessage(event); @@ -985,6 +986,7 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve << evt.matrixType(); }); } +#endif void Connection::Private::consumeDevicesList(DevicesList&& devicesList) { -- cgit v1.2.3 From c0c4cd014718fdb54ee691ccbdab46981e15d25f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Apr 2022 22:09:12 +0200 Subject: Use more idiomatic C++ --- lib/connection.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 1a5e3c6e..d99ab64d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -963,17 +963,14 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) #ifdef Quotient_E2EE_ENABLED void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) { - const auto [decryptedEvent, id] = sessionDecryptMessage(event); + const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); if(!decryptedEvent) { qCWarning(E2EE) << "Failed to decrypt event" << event.id(); return; } - // Yes, this is weird, but lgtm.com doesn't like it otherwise - const auto olmSessionId = id; - switchOnType(*decryptedEvent, - [this, senderKey = event.senderKey(), &event, olmSessionId](const RoomKeyEvent& roomKeyEvent) { + [this, senderKey = event.senderKey(), &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); } else { -- 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/connection.cpp | 4 ++-- lib/room.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 28377dd9..42a5f5fc 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -241,7 +241,7 @@ public: auto s = std::move(session); olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i); olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s)); - return { std::get(result), session->sessionId() }; + return { std::get(result), olmSessions[senderKey][0]->sessionId() }; } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; return {}; @@ -287,7 +287,7 @@ public: auto s = std::move(session); olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i); olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s)); - return { std::get(result), session->sessionId() }; + return { std::get(result), olmSessions[senderKey][0]->sessionId() }; } } qCWarning(E2EE) << "Failed to decrypt message"; 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 534b6f3fb3a4bd14d78942cab7eb430dd98e471c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 23 Apr 2022 19:46:58 +0200 Subject: SLICE() Add a macro to make slicing clear in the code and quiet for static analysis. --- lib/connection.cpp | 2 +- lib/e2ee/qolmmessage.cpp | 4 +++- lib/uri.cpp | 2 +- lib/util.h | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d99ab64d..a969b3b9 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2065,7 +2065,7 @@ void Connection::Private::loadOutdatedUserDevices() continue; } } - deviceKeys[user][device.deviceId] = device; + deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys); } outdatedUsers -= user; } diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp index 81b166b0..f9b4a5c2 100644 --- a/lib/e2ee/qolmmessage.cpp +++ b/lib/e2ee/qolmmessage.cpp @@ -4,6 +4,8 @@ #include "qolmmessage.h" +#include "util.h" + using namespace Quotient; QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) @@ -26,7 +28,7 @@ QOlmMessage::Type QOlmMessage::type() const QByteArray QOlmMessage::toCiphertext() const { - return QByteArray(*this); + return SLICE(*this, QByteArray); } QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) diff --git a/lib/uri.cpp b/lib/uri.cpp index 6b7d1d20..91751df0 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -171,7 +171,7 @@ QUrl Uri::toUrl(UriForm form) const return {}; if (form == CanonicalUri || type() == NonMatrix) - return *this; // NOLINT(cppcoreguidelines-slicing): It's intentional + return SLICE(*this, QUrl); QUrl url; url.setScheme("https"); diff --git a/lib/util.h b/lib/util.h index 753eb1ea..b14e1648 100644 --- a/lib/util.h +++ b/lib/util.h @@ -37,6 +37,22 @@ static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all QT_WARNING_POP #endif +/// \brief Copy an object with slicing +/// +/// Unintended slicing is bad, which why there's a C++ Core Guideline that +/// basically says "don't slice, or if you do, make it explicit". Sonar and +/// clang-tidy have warnings matching this guideline; unfortunately, those +/// warnings trigger even when you have a dedicated method (as the guideline +/// recommends) that makes a slicing copy. +/// +/// This macro is meant for cases when slicing is intended: the static cast +/// silences the static analysis warning, and the macro appearance itself makes +/// it very clear that slicing is wanted here. It is made as a macro +/// (not as a function template) to support the case of private inheritance +/// in which a function template would not be able to cast to the private base +/// (see Uri::toUrl() for an example of just that situation). +#define SLICE(Object, ToType) ToType{static_cast(Object)} + namespace Quotient { /// An equivalent of std::hash for QTypes to enable std::unordered_map template -- cgit v1.2.3 From 408785f0680a531c493de225fad60b6369227cfb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 23 Apr 2022 19:47:14 +0200 Subject: Cleanup --- lib/mxcreply.cpp | 1 - lib/util.h | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index b757bb93..319d514a 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -5,7 +5,6 @@ #include #include "accountregistry.h" -#include "connection.h" #include "room.h" #ifdef Quotient_E2EE_ENABLED diff --git a/lib/util.h b/lib/util.h index b14e1648..3910059b 100644 --- a/lib/util.h +++ b/lib/util.h @@ -109,8 +109,8 @@ private: */ template inline std::pair findFirstOf(InputIt first, InputIt last, - ForwardIt sFirst, - ForwardIt sLast, Pred pred) + ForwardIt sFirst, + ForwardIt sLast, Pred pred) { for (; first != last; ++first) for (auto it = sFirst; it != sLast; ++it) @@ -156,7 +156,7 @@ inline ImplPtr makeImpl(ArgTs&&... args) } template -const inline ImplPtr ZeroImpl() +constexpr ImplPtr ZeroImpl() { return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; } -- cgit v1.2.3 From 16d4f4e48304543a0ab59b235edba07f5f2c2204 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 18 Apr 2022 20:07:12 +0200 Subject: Implement key verification --- CMakeLists.txt | 3 + lib/connection.cpp | 104 +- lib/connection.h | 20 + lib/database.cpp | 30 +- lib/database.h | 4 + lib/events/encryptedevent.h | 2 + lib/events/keyverificationevent.cpp | 32 +- lib/events/keyverificationevent.h | 36 +- lib/events/roomevent.h | 2 +- lib/keyverificationsession.cpp | 522 +++++++++ lib/keyverificationsession.h | 140 +++ res.qrc | 5 + sas-emoji.json | 2178 +++++++++++++++++++++++++++++++++++ 13 files changed, 3066 insertions(+), 12 deletions(-) create mode 100644 lib/keyverificationsession.cpp create mode 100644 lib/keyverificationsession.h create mode 100644 res.qrc create mode 100644 sas-emoji.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 15726240..1030e12d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,7 @@ message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${ # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF) @@ -138,6 +139,7 @@ list(APPEND lib_SRCS lib/eventitem.h lib/eventitem.cpp lib/accountregistry.h lib/accountregistry.cpp lib/mxcreply.h lib/mxcreply.cpp + lib/keyverificationsession.h lib/keyverificationsession.cpp lib/events/event.h lib/events/event.cpp lib/events/eventloader.h lib/events/roomevent.h lib/events/roomevent.cpp @@ -171,6 +173,7 @@ list(APPEND lib_SRCS lib/jobs/syncjob.h lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.h lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.h lib/jobs/downloadfilejob.cpp + res.qrc ) if (${PROJECT_NAME}_ENABLE_E2EE) list(APPEND lib_SRCS diff --git a/lib/connection.cpp b/lib/connection.cpp index 42a5f5fc..68aed4e4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -40,6 +40,8 @@ # include "e2ee/qolmutils.h" # include "database.h" # include "e2ee/qolminboundsession.h" +# include "events/keyverificationevent.h" +# include "keyverificationsession.h" #if QT_VERSION_MAJOR >= 6 # include @@ -974,8 +976,23 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) outdatedUsers += event.senderId(); encryptionUpdateRequired = true; pendingEncryptedEvents.push_back(std::make_unique(event.fullJson())); - }, [](const Event& e){ - // Unhandled + }, [this](const KeyVerificationRequestEvent& event) { + auto session = new KeyVerificationSession(q->userId(), event, q, false, q); + emit q->newKeyVerificationSession(session); + }, [this](const KeyVerificationReadyEvent& event) { + emit q->incomingKeyVerificationReady(event); + }, [this](const KeyVerificationStartEvent& event) { + emit q->incomingKeyVerificationStart(event); + }, [this](const KeyVerificationAcceptEvent& event) { + emit q->incomingKeyVerificationAccept(event); + }, [this](const KeyVerificationKeyEvent& event) { + emit q->incomingKeyVerificationKey(event); + }, [this](const KeyVerificationMacEvent& event) { + emit q->incomingKeyVerificationMac(event); + }, [this](const KeyVerificationDoneEvent& event) { + emit q->incomingKeyVerificationDone(event); + }, [this](const KeyVerificationCancelEvent& event) { + emit q->incomingKeyVerificationCancel(event); }); } #endif @@ -998,9 +1015,25 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); } - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" + }, [this](const KeyVerificationRequestEvent& event) { + auto session = new KeyVerificationSession(q->userId(), event, q, true, q); + emit q->newKeyVerificationSession(session); + }, [this](const KeyVerificationReadyEvent& event) { + emit q->incomingKeyVerificationReady(event); + }, [this](const KeyVerificationStartEvent& event) { + emit q->incomingKeyVerificationStart(event); + }, [this](const KeyVerificationAcceptEvent& event) { + emit q->incomingKeyVerificationAccept(event); + }, [this](const KeyVerificationKeyEvent& event) { + emit q->incomingKeyVerificationKey(event); + }, [this](const KeyVerificationMacEvent& event) { + emit q->incomingKeyVerificationMac(event); + }, [this](const KeyVerificationDoneEvent& event) { + emit q->incomingKeyVerificationDone(event); + }, [this](const KeyVerificationCancelEvent& event) { + emit q->incomingKeyVerificationCancel(event); + }, [](const Event& evt) { + qCWarning(E2EE) << "Skipping encrypted to_device event, type" << evt.matrixType(); }); } @@ -2127,8 +2160,8 @@ void Connection::Private::saveDevicesList() query.prepare(QStringLiteral( "INSERT INTO tracked_devices" - "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) " - "VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) " + "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);" )); for (const auto& user : deviceKeys.keys()) { for (const auto& device : deviceKeys[user]) { @@ -2142,6 +2175,8 @@ void Connection::Private::saveDevicesList() query.bindValue(":curveKey", device.keys[curveKeyId]); query.bindValue(":edKeyId", edKeyId); query.bindValue(":edKey", device.keys[edKeyId]); + // If the device gets saved here, it can't be verified + query.bindValue(":verified", false); q->database()->execute(query); } @@ -2297,3 +2332,58 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) } #endif + +void Connection::startKeyVerificationSession(const QString& deviceId) +{ + auto session = new KeyVerificationSession(userId(), deviceId, this, this); + Q_EMIT newKeyVerificationSession(session); +} + +void Connection::sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt event, bool encrypted) +{ + + UsersToDevicesToEvents payload; + if (encrypted) { + QJsonObject payloadJson = event->fullJson(); + payloadJson["recipient"] = userId; + payloadJson["sender"] = user()->id(); + QJsonObject recipientObject; + recipientObject["ed25519"] = edKeyForUserDevice(userId, deviceId); + payloadJson["recipient_keys"] = recipientObject; + QJsonObject senderObject; + senderObject["ed25519"] = QString(olmAccount()->identityKeys().ed25519); + payloadJson["keys"] = senderObject; + + const auto& u = user(userId); + auto cipherText = olmEncryptMessage(u, deviceId, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encryptedJson; + encryptedJson[curveKeyForUserDevice(userId, deviceId)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}, {"sender", this->userId()}}; + auto encryptedEvent = makeEvent(encryptedJson, olmAccount()->identityKeys().curve25519); + payload[userId][deviceId] = std::move(encryptedEvent); + } else { + payload[userId][deviceId] = std::move(event); + } + sendToDevices(payload[userId][deviceId]->matrixType(), payload); +} + +bool Connection::isVerifiedSession(const QString& megolmSessionId) +{ + auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", megolmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto olmSessionId = query.value("olmSessionId").toString(); + query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", olmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto curveKey = query.value("senderKey"_ls).toString(); + query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + query.bindValue(":curveKey", curveKey); + database()->execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/connection.h b/lib/connection.h index 12db2e30..fc189ac4 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -26,6 +26,8 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" #include "e2ee/qolmoutboundsession.h" +#include "events/keyverificationevent.h" +#include "keyverificationsession.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -324,6 +326,8 @@ public: QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); + /// Returns true if this megolm session comes from a verified device + bool isVerifiedSession(const QString& megolmSessionId); //This assumes that an olm session with (user, device) exists QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); @@ -512,6 +516,9 @@ public: /// Saves the olm account data to disk. Usually doesn't need to be called manually. void saveOlmAccount(); + // This assumes that an olm session already exists. If it doesn't, no message is sent. + void sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt event, bool encrypted); + public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// @@ -688,6 +695,8 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); + void startKeyVerificationSession(const QString& deviceId); + #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); PicklingMode picklingMode() const; @@ -698,6 +707,7 @@ public Q_SLOTS: QString edKeyForUserDevice(const QString& user, const QString& device) const; bool isKnownCurveKey(const QString& user, const QString& curveKey); #endif + Q_SIGNALS: /// \brief Initial server resolution has failed /// @@ -855,6 +865,16 @@ Q_SIGNALS: void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); void devicesListLoaded(); + void incomingKeyVerificationReady(const KeyVerificationReadyEvent& event); + void incomingKeyVerificationStart(const KeyVerificationStartEvent& event); + void incomingKeyVerificationAccept(const KeyVerificationAcceptEvent& event); + void incomingKeyVerificationKey(const KeyVerificationKeyEvent& event); + void incomingKeyVerificationMac(const KeyVerificationMacEvent& event); + void incomingKeyVerificationDone(const KeyVerificationDoneEvent& event); + void incomingKeyVerificationCancel(const KeyVerificationCancelEvent& event); + + void newKeyVerificationSession(KeyVerificationSession* session); + void sessionVerified(const QString& userId, const QString& deviceId); protected: /** diff --git a/lib/database.cpp b/lib/database.cpp index 902d0487..a85d96bb 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -35,6 +35,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa case 1: migrateTo2(); case 2: migrateTo3(); case 3: migrateTo4(); + case 4: migrateTo5(); } } @@ -105,6 +106,7 @@ void Database::migrateTo2() { qCDebug(DATABASE) << "Migrating database to version 2"; transaction(); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); @@ -133,7 +135,7 @@ void Database::migrateTo3() void Database::migrateTo4() { - qCDebug(DATABASE) << "Migrating database to ersion 4"; + qCDebug(DATABASE) << "Migrating database to version 4"; transaction(); execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); @@ -143,6 +145,16 @@ void Database::migrateTo4() commit(); } +void Database::migrateTo5() +{ + qCDebug(DATABASE) << "Migrating database to version 5"; + transaction(); + + execute(QStringLiteral("ALTER TABLE tracked_devices ADD verified BOOL;")); + execute(QStringLiteral("PRAGMA user_version = 5")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); @@ -396,3 +408,19 @@ void Database::updateOlmSession(const QString& senderKey, const QString& session commit(); } +void Database::setSessionVerified(const QString& edKeyId) +{ + auto query = prepareQuery(QStringLiteral("UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;")); + query.bindValue(":edKeyId", edKeyId); + transaction(); + execute(query); + commit(); +} + +bool Database::isSessionVerified(const QString& edKey) +{ + auto query = prepareQuery(QStringLiteral("SELECT verified FROM tracked_devices WHERE edKey=:edKey")); + query.bindValue(":edKey", edKey); + execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/database.h b/lib/database.h index 3eb26b0a..afc41e42 100644 --- a/lib/database.h +++ b/lib/database.h @@ -50,11 +50,15 @@ public: QHash devicesWithoutKey(Room* room, const QString &sessionId); void setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index); + bool isSessionVerified(const QString& edKey); + void setSessionVerified(const QString& edKeyId); + private: void migrateTo1(); void migrateTo2(); void migrateTo3(); void migrateTo4(); + void migrateTo5(); QString m_matrixId; }; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index ddd5e415..bfacdec9 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -58,6 +58,8 @@ public: RoomEventPtr createDecrypted(const QString &decrypted) const; void setRelation(const QJsonObject& relation); + + bool isVerified(); }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp index 4803955d..e7f5b019 100644 --- a/lib/events/keyverificationevent.cpp +++ b/lib/events/keyverificationevent.cpp @@ -106,7 +106,7 @@ QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const return contentPart("short_authentification_string"_ls); } -QString KeyVerificationAcceptEvent::commitement() const +QString KeyVerificationAcceptEvent::commitment() const { return contentPart("commitment"_ls); } @@ -162,3 +162,33 @@ QHash KeyVerificationMacEvent::mac() const { return contentPart>("mac"_ls); } + +KeyVerificationDoneEvent::KeyVerificationDoneEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{ +} + +QString KeyVerificationDoneEvent::transactionId() const +{ + return contentPart("transaction_id"_ls); +} + + +KeyVerificationReadyEvent::KeyVerificationReadyEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationReadyEvent::fromDevice() const +{ + return contentPart("from_device"_ls); +} + +QString KeyVerificationReadyEvent::transactionId() const +{ + return contentPart("transaction_id"_ls); +} + +QStringList KeyVerificationReadyEvent::methods() const +{ + return contentPart("methods"_ls); +} diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 497e56a2..a9f63968 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan // SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once + #include "event.h" namespace Quotient { @@ -31,6 +33,24 @@ public: }; REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) +class QUOTIENT_API KeyVerificationReadyEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.ready", KeyVerificationReadyEvent) + + explicit KeyVerificationReadyEvent(const QJsonObject& obj); + + /// The device ID which is accepting the request. + QString fromDevice() const; + + /// The transaction id of the verification request + QString transactionId() const; + + /// The verification methods supported by the sender. + QStringList methods() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationReadyEvent) + + /// Begins a key verification process. class QUOTIENT_API KeyVerificationStartEvent : public Event { public: @@ -104,7 +124,7 @@ public: /// The hash (encoded as unpadded base64) of the concatenation of the /// device's ephemeral public key (encoded as unpadded base64) and the /// canonical JSON representation of the m.key.verification.start message. - QString commitement() const; + QString commitment() const; }; REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) @@ -128,7 +148,7 @@ REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. -class KeyVerificationKeyEvent : public Event { +class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) @@ -158,4 +178,16 @@ public: QHash mac() const; }; REGISTER_EVENT_TYPE(KeyVerificationMacEvent) + +class QUOTIENT_API KeyVerificationDoneEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationRequestEvent) + + explicit KeyVerificationDoneEvent(const QJsonObject& obj); + + /// The same transactionId as before + QString transactionId() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationDoneEvent) + } // namespace Quotient diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index a7d6c428..5670f55f 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -62,7 +62,7 @@ public: #ifdef Quotient_E2EE_ENABLED void setOriginalEvent(event_ptr_tt&& originalEvent); - const RoomEvent* originalEvent() { return _originalEvent.get(); } + const RoomEvent* originalEvent() const { return _originalEvent.get(); } const QJsonObject encryptedJson() const; #endif diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..3b3b7627 --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,522 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "connection.h" +#include "keyverificationsession.h" +#include "olm/sas.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "events/event.h" +#include +#include +#include +#include "database.h" + +using namespace Quotient; + +KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection *connection, bool encrypted, QObject* parent) + : QObject(parent) + , m_remoteUserId(remoteUserId) + , m_remoteDeviceId(event.fromDevice()) + , m_transactionId(event.transactionId()) + , m_connection(connection) + , m_encrypted(encrypted) + , m_remoteSupportedMethods(event.methods()) +{ + auto timeoutTime = std::min(event.timestamp() + 600000, QDateTime::currentDateTime().addSecs(120).toMSecsSinceEpoch()); + m_timeout = timeoutTime - QDateTime::currentMSecsSinceEpoch(); + if (m_timeout <= 5000) { + return; + } + init(); + setState(INCOMING); +} + +KeyVerificationSession::KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent) + : QObject(parent) + , m_remoteUserId(userId) + , m_remoteDeviceId(deviceId) + , m_transactionId(QUuid::createUuid().toString()) + , m_connection(connection) + , m_encrypted(false) +{ + m_timeout = 600000; + init(); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init() +{ + connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleReady(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationStart, this, [this](const KeyVerificationStartEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleStart(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationAccept, this, [this](const KeyVerificationAcceptEvent& event) { + if (event.transactionId() == m_transactionId) { + handleAccept(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationKey, this, [this](const KeyVerificationKeyEvent& event) { + if (event.transactionId() == m_transactionId) { + handleKey(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationMac, this, [this](const KeyVerificationMacEvent& event) { + if (event.transactionId() == m_transactionId) { + handleMac(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationDone, this, [this](const KeyVerificationDoneEvent& event) { + if (event.transactionId() == m_transactionId) { + handleDone(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationCancel, this, [this](const KeyVerificationCancelEvent& event) { + if (event.transactionId() == m_transactionId) { + handleCancel(event); + } + }); + + QTimer::singleShot(m_timeout, this, [this] { + cancelVerification(TIMEOUT); + }); + + + m_sas = olm_sas(new uint8_t[olm_sas_size()]); + auto randomSize = olm_create_sas_random_length(m_sas); + auto random = getRandom(randomSize); + olm_create_sas(m_sas, random.data(), randomSize); + + m_language = QLocale::system().uiLanguages()[0]; + m_language = m_language.left(m_language.indexOf('-')); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + delete[] reinterpret_cast(m_sas); +} + +void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) +{ + if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + olm_sas_set_their_key(m_sas, event.key().toLatin1().data(), event.key().toLatin1().size()); + + if (startSentByUs) { + auto commitment = QString(QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + if (commitment != m_commitment) { + qCWarning(E2EE) << "Commitment mismatch; aborting verification"; + cancelVerification(MISMATCHED_COMMITMENT); + return; + } + } else { + sendKey(); + } + setState(WAITINGFORVERIFICATION); + + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + QString key = QString(keyBytes); + + QByteArray output(6, '\0'); + QString infoTemplate = startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + auto info = infoTemplate.arg(m_connection->userId()).arg(m_connection->deviceId()).arg(key).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(event.key()).arg(m_transactionId); + olm_sas_generate_bytes(m_sas, info.toLatin1().data(), info.toLatin1().size(), output.data(), output.size()); + + QVector code(7, 0); + const auto& data = (uint8_t *) output.data(); + + code[0] = data[0] >> 2; + code[1] = (data[0] << 4 & 0x3f) | data[1] >> 4; + code[2] = (data[1] << 2 & 0x3f) | data[2] >> 6; + code[3] = data[2] & 0x3f; + code[4] = data[3] >> 2; + code[5] = (data[3] << 4 & 0x3f) | data[4] >> 4; + code[6] = (data[4] << 2 & 0x3f) | data[5] >> 6; + + for (const auto& c : code) { + auto [emoji, description] = emojiForCode(c); + QVariantMap map; + map["emoji"] = emoji; + map["description"] = description; + m_sasEmojis += map; + } + emit sasEmojisChanged(); + emit keyReceived(); +} + +QByteArray KeyVerificationSession::macInfo(bool verifying, const QString& key) +{ + return (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls).arg(m_connection->userId()).arg(m_connection->deviceId()).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(m_transactionId).arg(key).toLatin1(); +} + +QString KeyVerificationSession::calculateMac(const QString& input, bool verifying, const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), macInfo(verifying, keyId).data(), macInfo(verifying, keyId).size(), outputBytes.data(), outputBytes.size()); + auto output = QString(outputBytes); + return output.left(output.indexOf('=')); +} + +void KeyVerificationSession::sendMac() +{ + QString edKeyId = "ed25519:" % m_connection->deviceId(); + + auto keys = calculateMac(edKeyId, false); + + QJsonObject mac; + auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; + mac[edKeyId] = calculateMac(key, false, edKeyId); + + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.mac"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + {"keys", keys}, + {"mac", mac}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); +} + +void KeyVerificationSession::sendDone() +{ + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.done"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); +} + +void KeyVerificationSession::sendKey() +{ + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + QString key = QString(keyBytes); + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.key"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + {"key", key}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.cancel"}, + {"content", QJsonObject{ + {"code", errorToString(error)}, + {"reason", errorToString(error)}, + {"transaction_id", m_transactionId} + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.ready"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"methods", toJson(methods)}, + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.start"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"hashes", QJsonArray {"sha256"}}, + {"key_agreement_protocols", QJsonArray { "curve25519-hkdf-sha256" }}, + {"message_authentication_codes", QJsonArray { "hkdf-hmac-sha256" }}, + {"method", "m.sas.v1"}, + {"short_authentication_string", QJsonArray { "decimal", "emoji" }}, + {"transaction_id", m_transactionId}, + }} + }); + m_startEvent = QJsonDocument(event->contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(WAITINGFORACCEPT); +} + +void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) +{ + if (state() != WAITINGFORREADY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + setState(READY); + m_remoteSupportedMethods = event.methods(); + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) +{ + if (state() != READY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + if (startSentByUs) { + if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { + return; + } else { + startSentByUs = false; + } + } + QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size()); + const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact)); + auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + + auto acceptEvent = makeEvent(QJsonObject { + {"type", "m.key.verification.accept"}, + {"content", QJsonObject { + {"commitment", commitment}, + {"hash", "sha256"}, + {"key_agreement_protocol", "curve25519-hkdf-sha256"}, + {"message_authentication_code", "hkdf-hmac-sha256"}, + {"method", "m.sas.v1"}, + {"short_authentication_string", QJsonArray { + "decimal", + "emoji", + }}, + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(acceptEvent), m_encrypted); + setState(ACCEPTED); +} + +void KeyVerificationSession::handleAccept(const KeyVerificationAcceptEvent& event) +{ + if(state() != WAITINGFORACCEPT) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); +} + +void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +{ + QStringList keys = event.mac().keys(); + keys.sort(); + const auto& key = keys.join(","); + const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId; + + if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) { + cancelVerification(KEY_MISMATCH); + return; + } + + if (calculateMac(key, true) != event.keys()) { + cancelVerification(KEY_MISMATCH); + return; + } + + m_connection->database()->setSessionVerified(edKeyId); + emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); + macReceived = true; + + if (state() == WAITINGFORMAC) { + setState(DONE); + sendDone(); + emit finished(); + deleteLater(); + } +} + +void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent& event) +{ + if (state() != DONE) { + cancelVerification(UNEXPECTED_MESSAGE); + } +} + +void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& event) +{ + setError(stringToError(event.code())); + setState(CANCELED); +} + +std::pair KeyVerificationSession::emojiForCode(int code) +{ + static QJsonArray data; + if (data.isEmpty()) { + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + data = QJsonDocument::fromJson(dataFile.readAll()).array(); + } + if (data[code].toObject()["translated_descriptions"].toObject().contains(m_language)) { + return {data[code].toObject()["emoji"].toString(), data[code].toObject()["translated_descriptions"].toObject()[m_language].toString()}; + } + return {data[code].toObject()["emoji"].toString(), data[code].toObject()["description"].toString()}; +} + +QList KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + QJsonArray methods = toJson(m_supportedMethods); + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.request"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"transaction_id", m_transactionId}, + {"methods", methods}, + {"timestamp", QDateTime::currentMSecsSinceEpoch()}, + }}, + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(WAITINGFORREADY); +} + +KeyVerificationSession::State KeyVerificationSession::state() const +{ + return m_state; +} + +void KeyVerificationSession::setState(KeyVerificationSession::State state) +{ + m_state = state; + emit stateChanged(); +} + +KeyVerificationSession::Error KeyVerificationSession::error() const +{ + return m_error; +} + +void KeyVerificationSession::setError(Error error) +{ + m_error = error; + emit errorChanged(); +} + +QString KeyVerificationSession::errorToString(Error error) const +{ + switch(error) { + case NONE: + return "none"_ls; + case TIMEOUT: + return "m.timeout"_ls; + case USER: + return "m.user"_ls; + case UNEXPECTED_MESSAGE: + return "m.unexpected_message"_ls; + case UNKNOWN_TRANSACTION: + return "m.unknown_transaction"_ls; + case UNKNOWN_METHOD: + return "m.unknown_method"_ls; + case KEY_MISMATCH: + return "m.key_mismatch"_ls; + case USER_MISMATCH: + return "m.user_mismatch"_ls; + case INVALID_MESSAGE: + return "m.invalid_message"_ls; + case SESSION_ACCEPTED: + return "m.accepted"_ls; + case MISMATCHED_COMMITMENT: + return "m.mismatched_commitment"_ls; + case MISMATCHED_SAS: + return "m.mismatched_sas"_ls; + default: + return "m.user"_ls; + } +} + +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) const +{ + if (error == "m.timeout"_ls) { + return REMOTE_TIMEOUT; + } else if (error == "m.user"_ls) { + return REMOTE_USER; + } else if (error == "m.unexpected_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_transaction"_ls) { + return REMOTE_UNKNOWN_TRANSACTION; + } else if (error == "m.unknown_method"_ls) { + return REMOTE_UNKNOWN_METHOD; + } else if (error == "m.key_mismatch"_ls) { + return REMOTE_KEY_MISMATCH; + } else if (error == "m.user_mismatch"_ls) { + return REMOTE_USER_MISMATCH; + } else if (error == "m.invalid_message"_ls) { + return REMOTE_INVALID_MESSAGE; + } else if (error == "m.accepted"_ls) { + return REMOTE_SESSION_ACCEPTED; + } else if (error == "m.mismatched_commitment"_ls) { + return REMOTE_MISMATCHED_COMMITMENT; + } else if (error == "m.mismatched_sas"_ls) { + return REMOTE_MISMATCHED_SAS; + } + return NONE; +} + +QStringList KeyVerificationSession::commonSupportedMethods(const QStringList& remoteMethods) const +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (m_supportedMethods.contains(method)) { + result += method; + } + } + return result; +} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h new file mode 100644 index 00000000..cb7a99e9 --- /dev/null +++ b/lib/keyverificationsession.h @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/keyverificationevent.h" +#include +#include + +class OlmSAS; + +namespace Quotient { +class Connection; + +/** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. + Start a new session using Connection::startKeyVerificationSession. + The object is delete after finished is emitted. +*/ +class QUOTIENT_API KeyVerificationSession : public QObject +{ + Q_OBJECT + +public: + enum State { + INCOMING, // There is a request for verification incoming + WAITINGFORREADY, // We sent a request for verification and are waiting for ready + READY, // Either party sent a ready as a response to a request; The user selects a method + WAITINGFORACCEPT, // We sent a start and are waiting for an accept + ACCEPTED, // The other party sent an accept and is waiting for a key + WAITINGFORKEY, // We're waiting for a key + WAITINGFORVERIFICATION, // We're waiting for the *user* to verify the emojis + WAITINGFORMAC, // We're waiting for the mac + CANCELED, // The session has been canceled + DONE, // The verification is done + }; + Q_ENUM(State) + + enum Error { + NONE, + TIMEOUT, + REMOTE_TIMEOUT, + USER, + REMOTE_USER, + UNEXPECTED_MESSAGE, + REMOTE_UNEXPECTED_MESSAGE, + UNKNOWN_TRANSACTION, + REMOTE_UNKNOWN_TRANSACTION, + UNKNOWN_METHOD, + REMOTE_UNKNOWN_METHOD, + KEY_MISMATCH, + REMOTE_KEY_MISMATCH, + USER_MISMATCH, + REMOTE_USER_MISMATCH, + INVALID_MESSAGE, + REMOTE_INVALID_MESSAGE, + SESSION_ACCEPTED, + REMOTE_SESSION_ACCEPTED, + MISMATCHED_COMMITMENT, + REMOTE_MISMATCHED_COMMITMENT, + MISMATCHED_SAS, + REMOTE_MISMATCHED_SAS, + }; + Q_ENUM(Error); + + //Q_PROPERTY(int timeLeft READ timeLeft NOTIFY timeLeftChanged) + Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) + Q_PROPERTY(QList sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + + KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection* connection, bool encrypted, QObject* parent = nullptr); + KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent = nullptr); + ~KeyVerificationSession(); + + int timeLeft() const; + QList sasEmojis() const; + State state() const; + + Error error() const; + +public Q_SLOTS: + void sendRequest(); + void sendReady(); + void sendMac(); + void sendStartSas(); + void sendKey(); + void sendDone(); + void cancelVerification(Error error); + +Q_SIGNALS: + + void timeLeftChanged(); + void startReceived(); + void keyReceived(); + void sasEmojisChanged(); + void stateChanged(); + void errorChanged(); + void finished(); + +private: + QString m_remoteUserId; + QString m_remoteDeviceId; + QString m_transactionId; + Connection* m_connection; + OlmSAS* m_sas = nullptr; + int timeleft = 0; + QList m_sasEmojis; + bool startSentByUs = false; + State m_state; + Error m_error; + QString m_startEvent; + QString m_commitment; + QString m_language; + int m_timeout; + bool macReceived = false; + bool m_encrypted; + QStringList m_remoteSupportedMethods; + + void handleReady(const KeyVerificationReadyEvent& event); + void handleStart(const KeyVerificationStartEvent& event); + void handleAccept(const KeyVerificationAcceptEvent& event); + void handleKey(const KeyVerificationKeyEvent& event); + void handleMac(const KeyVerificationMacEvent& event); + void handleDone(const KeyVerificationDoneEvent& event); + void handleCancel(const KeyVerificationCancelEvent& event); + void init(); + void setState(State state); + void setError(Error error); + QStringList commonSupportedMethods(const QStringList& remoteSupportedMethods) const; + QString errorToString(Error error) const; + Error stringToError(const QString& error) const; + QStringList m_supportedMethods = { "m.sas.v1"_ls }; + + QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); + QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); + + std::pair emojiForCode(int code); +}; + +} diff --git a/res.qrc b/res.qrc new file mode 100644 index 00000000..f6769103 --- /dev/null +++ b/res.qrc @@ -0,0 +1,5 @@ + + + sas-emoji.json + + diff --git a/sas-emoji.json b/sas-emoji.json new file mode 100644 index 00000000..06e1e4b3 --- /dev/null +++ b/sas-emoji.json @@ -0,0 +1,2178 @@ +[ + { + "number": 0, + "emoji": "🐶", + "description": "Dog", + "unicode": "U+1F436", + "translated_descriptions": { + "ar": "كَلب", + "bg": "Куче", + "ca": "Gos", + "cs": "Pes", + "de": "Hund", + "eo": "Hundo", + "es": "Perro", + "et": "Koer", + "fi": "Koira", + "fr": "Chien", + "hr": "pas", + "hu": "Kutya", + "it": "Cane", + "ja": "犬", + "nb_NO": "Hund", + "nl": "Hond", + "pt_BR": "Cachorro", + "ru": "Собака", + "si": "බල්ලා", + "sk": "Hlava psa", + "sr": "пас", + "sv": "Hund", + "szl": null, + "tzm": "Aydi", + "uk": "Пес", + "zh_Hans": "狗" + } + }, + { + "number": 1, + "emoji": "🐱", + "description": "Cat", + "unicode": "U+1F431", + "translated_descriptions": { + "ar": "هِرَّة", + "bg": "Котка", + "ca": "Gat", + "cs": "Kočka", + "de": "Katze", + "eo": "Kato", + "es": "Gato", + "et": "Kass", + "fi": "Kissa", + "fr": "Chat", + "hr": "mačka", + "hu": "Macska", + "it": "Gatto", + "ja": "猫", + "nb_NO": "Katt", + "nl": "Kat", + "pt_BR": "Gato", + "ru": "Кошка", + "si": "පූසා", + "sk": "Hlava mačky", + "sr": "мачка", + "sv": "Katt", + "szl": null, + "tzm": "Amuc", + "uk": "Кіт", + "zh_Hans": "猫" + } + }, + { + "number": 2, + "emoji": "🦁", + "description": "Lion", + "unicode": "U+1F981", + "translated_descriptions": { + "ar": "أَسَد", + "bg": "Лъв", + "ca": "Lleó", + "cs": "Lev", + "de": "Löwe", + "eo": "Leono", + "es": "León", + "et": "Lõvi", + "fi": "Leijona", + "fr": "Lion", + "hr": "lav", + "hu": "Oroszlán", + "it": "Leone", + "ja": "ライオン", + "nb_NO": "Løve", + "nl": "Leeuw", + "pt_BR": "Leão", + "ru": "Лев", + "si": "සිංහයා", + "sk": "Hlava leva", + "sr": "лав", + "sv": "Lejon", + "szl": null, + "tzm": "Izem", + "uk": "Лев", + "zh_Hans": "狮子" + } + }, + { + "number": 3, + "emoji": "🐎", + "description": "Horse", + "unicode": "U+1F40E", + "translated_descriptions": { + "ar": "حِصَان", + "bg": "Кон", + "ca": "Cavall", + "cs": "Kůň", + "de": "Pferd", + "eo": "Ĉevalo", + "es": "Caballo", + "et": "Hobune", + "fi": "Hevonen", + "fr": "Cheval", + "hr": "konj", + "hu": "Ló", + "it": "Cavallo", + "ja": "馬", + "nb_NO": "Hest", + "nl": "Paard", + "pt_BR": "Cavalo", + "ru": "Лошадь", + "si": "අශ්වයා", + "sk": "Kôň", + "sr": "коњ", + "sv": "Häst", + "szl": null, + "tzm": "Ayyis", + "uk": "Кінь", + "zh_Hans": "马" + } + }, + { + "number": 4, + "emoji": "🦄", + "description": "Unicorn", + "unicode": "U+1F984", + "translated_descriptions": { + "ar": "حِصَانٌ بِقَرن", + "bg": "Еднорог", + "ca": "Unicorn", + "cs": "Jednorožec", + "de": "Einhorn", + "eo": "Unukorno", + "es": "Unicornio", + "et": "Ükssarvik", + "fi": "Yksisarvinen", + "fr": "Licorne", + "hr": "jednorog", + "hu": "Egyszarvú", + "it": "Unicorno", + "ja": "ユニコーン", + "nb_NO": "Enhjørning", + "nl": "Eenhoorn", + "pt_BR": "Unicórnio", + "ru": "Единорог", + "si": null, + "sk": "Hlava jednorožca", + "sr": "једнорог", + "sv": "Enhörning", + "szl": null, + "tzm": null, + "uk": "Єдиноріг", + "zh_Hans": "独角兽" + } + }, + { + "number": 5, + "emoji": "🐷", + "description": "Pig", + "unicode": "U+1F437", + "translated_descriptions": { + "ar": "خِنزِير", + "bg": "Прасе", + "ca": "Porc", + "cs": "Prase", + "de": "Schwein", + "eo": "Porko", + "es": "Cerdo", + "et": "Siga", + "fi": "Sika", + "fr": "Cochon", + "hr": "svinja", + "hu": "Malac", + "it": "Maiale", + "ja": "ブタ", + "nb_NO": "Gris", + "nl": "Varken", + "pt_BR": "Porco", + "ru": "Свинья", + "si": null, + "sk": "Hlava prasaťa", + "sr": "прасе", + "sv": "Gris", + "szl": null, + "tzm": "Ilef", + "uk": "Свиня", + "zh_Hans": "猪" + } + }, + { + "number": 6, + "emoji": "🐘", + "description": "Elephant", + "unicode": "U+1F418", + "translated_descriptions": { + "ar": "فِيل", + "bg": "Слон", + "ca": "Elefant", + "cs": "Slon", + "de": "Elefant", + "eo": "Elefanto", + "es": "Elefante", + "et": "Elevant", + "fi": "Norsu", + "fr": "Éléphant", + "hr": "slon", + "hu": "Elefánt", + "it": "Elefante", + "ja": "ゾウ", + "nb_NO": "Elefant", + "nl": "Olifant", + "pt_BR": "Elefante", + "ru": "Слон", + "si": null, + "sk": "Slon", + "sr": "слон", + "sv": "Elefant", + "szl": null, + "tzm": "Ilu", + "uk": "Слон", + "zh_Hans": "大象" + } + }, + { + "number": 7, + "emoji": "🐰", + "description": "Rabbit", + "unicode": "U+1F430", + "translated_descriptions": { + "ar": "أَرنَب", + "bg": "Заек", + "ca": "Conill", + "cs": "Králík", + "de": "Hase", + "eo": "Kuniklo", + "es": "Conejo", + "et": "Jänes", + "fi": "Kani", + "fr": "Lapin", + "hr": "zec", + "hu": "Nyúl", + "it": "Coniglio", + "ja": "うさぎ", + "nb_NO": "Kanin", + "nl": "Konijn", + "pt_BR": "Coelho", + "ru": "Кролик", + "si": null, + "sk": "Hlava zajaca", + "sr": "зец", + "sv": "Kanin", + "szl": null, + "tzm": "Agnin", + "uk": "Кріль", + "zh_Hans": "兔子" + } + }, + { + "number": 8, + "emoji": "🐼", + "description": "Panda", + "unicode": "U+1F43C", + "translated_descriptions": { + "ar": "باندَا", + "bg": "Панда", + "ca": "Panda", + "cs": "Panda", + "de": "Panda", + "eo": "Pando", + "es": "Panda", + "et": "Panda", + "fi": "Panda", + "fr": "Panda", + "hr": "panda", + "hu": "Panda", + "it": "Panda", + "ja": "パンダ", + "nb_NO": "Panda", + "nl": "Panda", + "pt_BR": "Panda", + "ru": "Панда", + "si": null, + "sk": "Hlava pandy", + "sr": "панда", + "sv": "Panda", + "szl": null, + "tzm": null, + "uk": "Панда", + "zh_Hans": "熊猫" + } + }, + { + "number": 9, + "emoji": "🐓", + "description": "Rooster", + "unicode": "U+1F413", + "translated_descriptions": { + "ar": "دِيك", + "bg": "Петел", + "ca": "Gall", + "cs": "Kohout", + "de": "Hahn", + "eo": "Virkoko", + "es": "Gallo", + "et": "Kukk", + "fi": "Kukko", + "fr": "Coq", + "hr": "kokot", + "hu": "Kakas", + "it": "Gallo", + "ja": "ニワトリ", + "nb_NO": "Hane", + "nl": "Haan", + "pt_BR": "Galo", + "ru": "Петух", + "si": null, + "sk": "Kohút", + "sr": "петао", + "sv": "Tupp", + "szl": null, + "tzm": "Ayaẓiḍ", + "uk": "Когут", + "zh_Hans": "公鸡" + } + }, + { + "number": 10, + "emoji": "🐧", + "description": "Penguin", + "unicode": "U+1F427", + "translated_descriptions": { + "ar": "بِطريق", + "bg": "Пингвин", + "ca": "Pingüí", + "cs": "Tučňák", + "de": "Pinguin", + "eo": "Pingveno", + "es": "Pingüino", + "et": "Pingviin", + "fi": "Pingviini", + "fr": "Manchot", + "hr": "pingvin", + "hu": "Pingvin", + "it": "Pinguino", + "ja": "ペンギン", + "nb_NO": "Pingvin", + "nl": "Pinguïn", + "pt_BR": "Pinguim", + "ru": "Пингвин", + "si": null, + "sk": "Tučniak", + "sr": "пингвин", + "sv": "Pingvin", + "szl": null, + "tzm": null, + "uk": "Пінгвін", + "zh_Hans": "企鹅" + } + }, + { + "number": 11, + "emoji": "🐢", + "description": "Turtle", + "unicode": "U+1F422", + "translated_descriptions": { + "ar": "سُلحفاة", + "bg": "Костенурка", + "ca": "Tortuga", + "cs": "Želva", + "de": "Schildkröte", + "eo": "Testudo", + "es": "Tortuga", + "et": "Kilpkonn", + "fi": "Kilpikonna", + "fr": "Tortue", + "hr": "kornjača", + "hu": "Teknős", + "it": "Tartaruga", + "ja": "亀", + "nb_NO": "Skilpadde", + "nl": "Schildpad", + "pt_BR": "Tartaruga", + "ru": "Черепаха", + "si": null, + "sk": "Korytnačka", + "sr": "корњача", + "sv": "Sköldpadda", + "szl": null, + "tzm": "Ifker", + "uk": "Черепаха", + "zh_Hans": "乌龟" + } + }, + { + "number": 12, + "emoji": "🐟", + "description": "Fish", + "unicode": "U+1F41F", + "translated_descriptions": { + "ar": "سَمَكَة", + "bg": "Риба", + "ca": "Peix", + "cs": "Ryba", + "de": "Fisch", + "eo": "Fiŝo", + "es": "Pez", + "et": "Kala", + "fi": "Kala", + "fr": "Poisson", + "hr": "riba", + "hu": "Hal", + "it": "Pesce", + "ja": "魚", + "nb_NO": "Fisk", + "nl": "Vis", + "pt_BR": "Peixe", + "ru": "Рыба", + "si": null, + "sk": "Ryba", + "sr": "риба", + "sv": "Fisk", + "szl": null, + "tzm": "Aselm", + "uk": "Риба", + "zh_Hans": "鱼" + } + }, + { + "number": 13, + "emoji": "🐙", + "description": "Octopus", + "unicode": "U+1F419", + "translated_descriptions": { + "ar": "أُخطُبُوط", + "bg": "Октопод", + "ca": "Pop", + "cs": "Chobotnice", + "de": "Oktopus", + "eo": "Polpo", + "es": "Pulpo", + "et": "Kaheksajalg", + "fi": "Tursas", + "fr": "Poulpe", + "hr": "hobotnica", + "hu": "Polip", + "it": "Polpo", + "ja": "たこ", + "nb_NO": "Blekksprut", + "nl": "Octopus", + "pt_BR": "Polvo", + "ru": "Осьминог", + "si": null, + "sk": "Chobotnica", + "sr": "октопод", + "sv": "Bläckfisk", + "szl": null, + "tzm": null, + "uk": "Восьминіг", + "zh_Hans": "章鱼" + } + }, + { + "number": 14, + "emoji": "🦋", + "description": "Butterfly", + "unicode": "U+1F98B", + "translated_descriptions": { + "ar": "فَرَاشَة", + "bg": "Пеперуда", + "ca": "Papallona", + "cs": "Motýl", + "de": "Schmetterling", + "eo": "Papilio", + "es": "Mariposa", + "et": "Liblikas", + "fi": "Perhonen", + "fr": "Papillon", + "hr": "leptir", + "hu": "Pillangó", + "it": "Farfalla", + "ja": "ちょうちょ", + "nb_NO": "Sommerfugl", + "nl": "Vlinder", + "pt_BR": "Borboleta", + "ru": "Бабочка", + "si": null, + "sk": "Motýľ", + "sr": "лептир", + "sv": "Fjäril", + "szl": null, + "tzm": null, + "uk": "Метелик", + "zh_Hans": "蝴蝶" + } + }, + { + "number": 15, + "emoji": "🌷", + "description": "Flower", + "unicode": "U+1F337", + "translated_descriptions": { + "ar": "زَهرَة", + "bg": "Цвете", + "ca": "Flor", + "cs": "Květina", + "de": "Blume", + "eo": "Floro", + "es": "Flor", + "et": "Lill", + "fi": "Kukka", + "fr": "Fleur", + "hr": "svijet", + "hu": "Virág", + "it": "Fiore", + "ja": "花", + "nb_NO": "Blomst", + "nl": "Bloem", + "pt_BR": "Flor", + "ru": "Цветок", + "si": null, + "sk": "Tulipán", + "sr": "цвет", + "sv": "Blomma", + "szl": null, + "tzm": null, + "uk": "Квітка", + "zh_Hans": "花" + } + }, + { + "number": 16, + "emoji": "🌳", + "description": "Tree", + "unicode": "U+1F333", + "translated_descriptions": { + "ar": "شَجَرَة", + "bg": "Дърво", + "ca": "Arbre", + "cs": "Strom", + "de": "Baum", + "eo": "Arbo", + "es": "Árbol", + "et": "Puu", + "fi": "Puu", + "fr": "Arbre", + "hr": "drvo", + "hu": "Fa", + "it": "Albero", + "ja": "木", + "nb_NO": "Tre", + "nl": "Boom", + "pt_BR": "Árvore", + "ru": "Дерево", + "si": null, + "sk": "Listnatý strom", + "sr": "дрво", + "sv": "Träd", + "szl": null, + "tzm": "Aseklu", + "uk": "Дерево", + "zh_Hans": "树" + } + }, + { + "number": 17, + "emoji": "🌵", + "description": "Cactus", + "unicode": "U+1F335", + "translated_descriptions": { + "ar": "صبار", + "bg": "Кактус", + "ca": "Cactus", + "cs": "Kaktus", + "de": "Kaktus", + "eo": "Kakto", + "es": "Cactus", + "et": "Kaktus", + "fi": "Kaktus", + "fr": "Cactus", + "hr": "kaktus", + "hu": "Kaktusz", + "it": "Cactus", + "ja": "サボテン", + "nb_NO": "Kaktus", + "nl": "Cactus", + "pt_BR": "Cacto", + "ru": "Кактус", + "si": null, + "sk": "Kaktus", + "sr": "кактус", + "sv": "Kaktus", + "szl": null, + "tzm": null, + "uk": "Кактус", + "zh_Hans": "仙人掌" + } + }, + { + "number": 18, + "emoji": "🍄", + "description": "Mushroom", + "unicode": "U+1F344", + "translated_descriptions": { + "ar": "فُطر", + "bg": "Гъба", + "ca": "Bolet", + "cs": "Houba", + "de": "Pilz", + "eo": "Fungo", + "es": "Seta", + "et": "Seen", + "fi": "Sieni", + "fr": "Champignon", + "hr": "gljiva", + "hu": "Gomba", + "it": "Fungo", + "ja": "きのこ", + "nb_NO": "Sopp", + "nl": "Paddenstoel", + "pt_BR": "Cogumelo", + "ru": "Гриб", + "si": null, + "sk": "Huba", + "sr": "печурка", + "sv": "Svamp", + "szl": null, + "tzm": "Agursel", + "uk": "Гриб", + "zh_Hans": "蘑菇" + } + }, + { + "number": 19, + "emoji": "🌏", + "description": "Globe", + "unicode": "U+1F30F", + "translated_descriptions": { + "ar": "كُرَةٌ أرضِيَّة", + "bg": "Глобус", + "ca": "Globus terraqüi", + "cs": "Zeměkoule", + "de": "Globus", + "eo": "Globo", + "es": "Globo", + "et": "Maakera", + "fi": "Maapallo", + "fr": "Globe", + "hr": "Globus", + "hu": "Földgömb", + "it": "Globo", + "ja": "地球", + "nb_NO": "Globus", + "nl": "Wereldbol", + "pt_BR": "Globo", + "ru": "Глобус", + "si": null, + "sk": "Zemeguľa", + "sr": "глобус", + "sv": "Jordklot", + "szl": null, + "tzm": null, + "uk": "Глобус", + "zh_Hans": "地球" + } + }, + { + "number": 20, + "emoji": "🌙", + "description": "Moon", + "unicode": "U+1F319", + "translated_descriptions": { + "ar": "قَمَر", + "bg": "Луна", + "ca": "Lluna", + "cs": "Měsíc", + "de": "Mond", + "eo": "Luno", + "es": "Luna", + "et": "Kuu", + "fi": "Kuu", + "fr": "Lune", + "hr": "mjesec", + "hu": "Hold", + "it": "Luna", + "ja": "月", + "nb_NO": "Måne", + "nl": "Maan", + "pt_BR": "Lua", + "ru": "Луна", + "si": null, + "sk": "Polmesiac", + "sr": "месец", + "sv": "Måne", + "szl": null, + "tzm": "Ayyur", + "uk": "Місяць", + "zh_Hans": "月亮" + } + }, + { + "number": 21, + "emoji": "☁️", + "description": "Cloud", + "unicode": "U+2601U+FE0F", + "translated_descriptions": { + "ar": "سَحابَة", + "bg": "Облак", + "ca": "Núvol", + "cs": "Mrak", + "de": "Wolke", + "eo": "Nubo", + "es": "Nube", + "et": "Pilv", + "fi": "Pilvi", + "fr": "Nuage", + "hr": "oblak", + "hu": "Felhő", + "it": "Nuvola", + "ja": "雲", + "nb_NO": "Sky", + "nl": "Wolk", + "pt_BR": "Nuvem", + "ru": "Облако", + "si": null, + "sk": "Oblak", + "sr": "облак", + "sv": "Moln", + "szl": null, + "tzm": null, + "uk": "Хмара", + "zh_Hans": "云" + } + }, + { + "number": 22, + "emoji": "🔥", + "description": "Fire", + "unicode": "U+1F525", + "translated_descriptions": { + "ar": "نار", + "bg": "Огън", + "ca": "Foc", + "cs": "Oheň", + "de": "Feuer", + "eo": "Fajro", + "es": "Fuego", + "et": "Tuli", + "fi": "Tuli", + "fr": "Feu", + "hr": "vatra", + "hu": "Tűz", + "it": "Fuoco", + "ja": "炎", + "nb_NO": "Flamme", + "nl": "Vuur", + "pt_BR": "Fogo", + "ru": "Огонь", + "si": null, + "sk": "Oheň", + "sr": "ватра", + "sv": "Eld", + "szl": null, + "tzm": "Timessi", + "uk": "Вогонь", + "zh_Hans": "火" + } + }, + { + "number": 23, + "emoji": "🍌", + "description": "Banana", + "unicode": "U+1F34C", + "translated_descriptions": { + "ar": "مَوزَة", + "bg": "Банан", + "ca": "Plàtan", + "cs": "Banán", + "de": "Banane", + "eo": "Banano", + "es": "Plátano", + "et": "Banaan", + "fi": "Banaani", + "fr": "Banane", + "hr": "banana", + "hu": "Banán", + "it": "Banana", + "ja": "バナナ", + "nb_NO": "Banan", + "nl": "Banaan", + "pt_BR": "Banana", + "ru": "Банан", + "si": null, + "sk": "Banán", + "sr": "банана", + "sv": "Banan", + "szl": null, + "tzm": "Tabanant", + "uk": "Банан", + "zh_Hans": "香蕉" + } + }, + { + "number": 24, + "emoji": "🍎", + "description": "Apple", + "unicode": "U+1F34E", + "translated_descriptions": { + "ar": "تُفَّاحَة", + "bg": "Ябълка", + "ca": "Poma", + "cs": "Jablko", + "de": "Apfel", + "eo": "Pomo", + "es": "Manzana", + "et": "Õun", + "fi": "Omena", + "fr": "Pomme", + "hr": "jabuka", + "hu": "Alma", + "it": "Mela", + "ja": "リンゴ", + "nb_NO": "Eple", + "nl": "Appel", + "pt_BR": "Maçã", + "ru": "Яблоко", + "si": null, + "sk": "Červené jablko", + "sr": "јабука", + "sv": "Äpple", + "szl": null, + "tzm": "Tadeffuyt", + "uk": "Яблуко", + "zh_Hans": "苹果" + } + }, + { + "number": 25, + "emoji": "🍓", + "description": "Strawberry", + "unicode": "U+1F353", + "translated_descriptions": { + "ar": "فَراوِلَة", + "bg": "Ягода", + "ca": "Maduixa", + "cs": "Jahoda", + "de": "Erdbeere", + "eo": "Frago", + "es": "Fresa", + "et": "Maasikas", + "fi": "Mansikka", + "fr": "Fraise", + "hr": "jagoda", + "hu": "Eper", + "it": "Fragola", + "ja": "いちご", + "nb_NO": "Jordbær", + "nl": "Aardbei", + "pt_BR": "Morango", + "ru": "Клубника", + "si": null, + "sk": "Jahoda", + "sr": "јагода", + "sv": "Jordgubbe", + "szl": null, + "tzm": null, + "uk": "Полуниця", + "zh_Hans": "草莓" + } + }, + { + "number": 26, + "emoji": "🌽", + "description": "Corn", + "unicode": "U+1F33D", + "translated_descriptions": { + "ar": "ذُرَة", + "bg": "Царевица", + "ca": "Blat de moro", + "cs": "Kukuřice", + "de": "Mais", + "eo": "Maizo", + "es": "Maíz", + "et": "Mais", + "fi": "Maissi", + "fr": "Maïs", + "hr": "kukuruza", + "hu": "Kukorica", + "it": "Mais", + "ja": "とうもろこし", + "nb_NO": "Mais", + "nl": "Maïs", + "pt_BR": "Milho", + "ru": "Кукуруза", + "si": null, + "sk": "Kukuričný klas", + "sr": "кукуруз", + "sv": "Majs", + "szl": null, + "tzm": null, + "uk": "Кукурудза", + "zh_Hans": "玉米" + } + }, + { + "number": 27, + "emoji": "🍕", + "description": "Pizza", + "unicode": "U+1F355", + "translated_descriptions": { + "ar": "بِيتزا", + "bg": "Пица", + "ca": "Pizza", + "cs": "Pizza", + "de": "Pizza", + "eo": "Pico", + "es": "Pizza", + "et": "Pitsa", + "fi": "Pizza", + "fr": "Pizza", + "hr": "pizza", + "hu": "Pizza", + "it": "Pizza", + "ja": "ピザ", + "nb_NO": "Pizza", + "nl": "Pizza", + "pt_BR": "Pizza", + "ru": "Пицца", + "si": null, + "sk": "Pizza", + "sr": "пица", + "sv": "Pizza", + "szl": null, + "tzm": null, + "uk": "Піца", + "zh_Hans": "披萨" + } + }, + { + "number": 28, + "emoji": "🎂", + "description": "Cake", + "unicode": "U+1F382", + "translated_descriptions": { + "ar": "كَعكَة", + "bg": "Торта", + "ca": "Pastís", + "cs": "Dort", + "de": "Kuchen", + "eo": "Torto", + "es": "Tarta", + "et": "Kook", + "fi": "Kakku", + "fr": "Gâteau", + "hr": "torta", + "hu": "Süti", + "it": "Torta", + "ja": "ケーキ", + "nb_NO": "Kake", + "nl": "Taart", + "pt_BR": "Bolo", + "ru": "Торт", + "si": null, + "sk": "Narodeninová torta", + "sr": "торта", + "sv": "Tårta", + "szl": null, + "tzm": null, + "uk": "Пиріг", + "zh_Hans": "蛋糕" + } + }, + { + "number": 29, + "emoji": "❤️", + "description": "Heart", + "unicode": "U+2764U+FE0F", + "translated_descriptions": { + "ar": "قَلب", + "bg": "Сърце", + "ca": "Cor", + "cs": "Srdce", + "de": "Herz", + "eo": "Koro", + "es": "Corazón", + "et": "Süda", + "fi": "Sydän", + "fr": "Cœur", + "hr": "srca", + "hu": "Szív", + "it": "Cuore", + "ja": "ハート", + "nb_NO": "Hjerte", + "nl": "Hart", + "pt_BR": "Coração", + "ru": "Сердце", + "si": null, + "sk": "červené srdce", + "sr": "срце", + "sv": "Hjärta", + "szl": null, + "tzm": "Ul", + "uk": "Серце", + "zh_Hans": "心" + } + }, + { + "number": 30, + "emoji": "😀", + "description": "Smiley", + "unicode": "U+1F600", + "translated_descriptions": { + "ar": "اِبتِسَامَة", + "bg": "Усмивка", + "ca": "Somrient", + "cs": "Smajlík", + "de": "Lächeln", + "eo": "Rideto", + "es": "Emoticono", + "et": "Smaili", + "fi": "Hymynaama", + "fr": "Sourire", + "hr": "smajlića", + "hu": "Mosoly", + "it": "Faccina sorridente", + "ja": "スマイル", + "nb_NO": "Smilefjes", + "nl": "Smiley", + "pt_BR": "Sorriso", + "ru": "Улыбка", + "si": null, + "sk": "Škeriaca sa tvár", + "sr": "смајли", + "sv": "Smiley", + "szl": null, + "tzm": null, + "uk": "Посмішка", + "zh_Hans": "笑脸" + } + }, + { + "number": 31, + "emoji": "🤖", + "description": "Robot", + "unicode": "U+1F916", + "translated_descriptions": { + "ar": "رُوبُوت", + "bg": "Робот", + "ca": "Robot", + "cs": "Robot", + "de": "Roboter", + "eo": "Roboto", + "es": "Robot", + "et": "Robot", + "fi": "Robotti", + "fr": "Robot", + "hr": "robot", + "hu": "Robot", + "it": "Robot", + "ja": "ロボと", + "nb_NO": "Robot", + "nl": "Robot", + "pt_BR": "Robô", + "ru": "Робот", + "si": null, + "sk": "Robot", + "sr": "робот", + "sv": "Robot", + "szl": null, + "tzm": "Aṛubu", + "uk": "Робот", + "zh_Hans": "机器人" + } + }, + { + "number": 32, + "emoji": "🎩", + "description": "Hat", + "unicode": "U+1F3A9", + "translated_descriptions": { + "ar": "قُبَّعَة", + "bg": "Шапка", + "ca": "Barret", + "cs": "Klobouk", + "de": "Hut", + "eo": "Ĉapelo", + "es": "Sombrero", + "et": "Kübar", + "fi": "Hattu", + "fr": "Chapeau", + "hr": "kapa", + "hu": "Kalap", + "it": "Cappello", + "ja": "帽子", + "nb_NO": "Hatt", + "nl": "Hoed", + "pt_BR": "Chapéu", + "ru": "Шляпа", + "si": null, + "sk": "Cilinder", + "sr": "шешир", + "sv": "Hatt", + "szl": null, + "tzm": "Taraza", + "uk": "Капелюх", + "zh_Hans": "帽子" + } + }, + { + "number": 33, + "emoji": "👓", + "description": "Glasses", + "unicode": "U+1F453", + "translated_descriptions": { + "ar": "نَظَّارَة", + "bg": "Очила", + "ca": "Ulleres", + "cs": "Brýle", + "de": "Brille", + "eo": "Okulvitroj", + "es": "Gafas", + "et": "Prillid", + "fi": "Silmälasit", + "fr": "Lunettes", + "hr": "naočale", + "hu": "Szemüveg", + "it": "Occhiali", + "ja": "めがね", + "nb_NO": "Briller", + "nl": "Bril", + "pt_BR": "Óculos", + "ru": "Очки", + "si": null, + "sk": "Okuliare", + "sr": "наочаре", + "sv": "Glasögon", + "szl": null, + "tzm": null, + "uk": "Окуляри", + "zh_Hans": "眼镜" + } + }, + { + "number": 34, + "emoji": "🔧", + "description": "Spanner", + "unicode": "U+1F527", + "translated_descriptions": { + "ar": "مِفتَاحُ رَبط", + "bg": "Гаечен ключ", + "ca": "Clau anglesa", + "cs": "Klíč", + "de": "Schraubenschlüssel", + "eo": "Ŝraŭbŝlosilo", + "es": "Llave inglesa", + "et": "Mutrivõti", + "fi": "Kiintoavain", + "fr": "Clé à molette", + "hr": "ključ", + "hu": "Csavarkulcs", + "it": "Chiave inglese", + "ja": "スパナ", + "nb_NO": "Fastnøkkel", + "nl": "Moersleutel", + "pt_BR": "Chave inglesa", + "ru": "Ключ", + "si": null, + "sk": "Francúzsky kľúč", + "sr": "кључ", + "sv": "Skruvnyckel", + "szl": null, + "tzm": null, + "uk": "Гайковий ключ", + "zh_Hans": "扳手" + } + }, + { + "number": 35, + "emoji": "🎅", + "description": "Santa", + "unicode": "U+1F385", + "translated_descriptions": { + "ar": "سانتا", + "bg": "Дядо Коледа", + "ca": "Pare Noél", + "cs": "Mikuláš", + "de": "Weihnachtsmann", + "eo": "Kristnaska viro", + "es": "Papá Noel", + "et": "Jõuluvana", + "fi": "Joulupukki", + "fr": "Père Noël", + "hr": "deda Mraz", + "hu": "Télapó", + "it": "Babbo Natale", + "ja": "サンタ", + "nb_NO": "Julenisse", + "nl": "Kerstman", + "pt_BR": "Papai-noel", + "ru": "Санта", + "si": null, + "sk": "Santa Claus", + "sr": "деда Мраз", + "sv": "Tomte", + "szl": null, + "tzm": null, + "uk": "Санта Клаус", + "zh_Hans": "圣诞老人" + } + }, + { + "number": 36, + "emoji": "👍", + "description": "Thumbs Up", + "unicode": "U+1F44D", + "translated_descriptions": { + "ar": "رَفعُ إِبهَام", + "bg": "Палец нагоре", + "ca": "Polzes amunt", + "cs": "Palec nahoru", + "de": "Daumen Hoch", + "eo": "Dikfingro supren", + "es": "Pulgar arriba", + "et": "Pöidlad püsti", + "fi": "Peukalo ylös", + "fr": "Pouce en l’air", + "hr": "palac gore", + "hu": "Hüvelykujj fel", + "it": "Pollice alzato", + "ja": "いいね", + "nb_NO": "Tommel Opp", + "nl": "Duim omhoog", + "pt_BR": "Joinha", + "ru": "Большой палец вверх", + "si": null, + "sk": "Palec nahor", + "sr": "палчић горе", + "sv": "Tummen upp", + "szl": null, + "tzm": null, + "uk": "Великий палець вгору", + "zh_Hans": "赞" + } + }, + { + "number": 37, + "emoji": "☂️", + "description": "Umbrella", + "unicode": "U+2602U+FE0F", + "translated_descriptions": { + "ar": "مِظَلَّة", + "bg": "Чадър", + "ca": "Paraigües", + "cs": "Deštník", + "de": "Regenschirm", + "eo": "Ombrelo", + "es": "Paraguas", + "et": "Vihmavari", + "fi": "Sateenvarjo", + "fr": "Parapluie", + "hr": "kišobran", + "hu": "Esernyő", + "it": "Ombrello", + "ja": "傘", + "nb_NO": "Paraply", + "nl": "Paraplu", + "pt_BR": "Guarda-chuva", + "ru": "Зонт", + "si": null, + "sk": "Dáždnik", + "sr": "кишобран", + "sv": "Paraply", + "szl": null, + "tzm": null, + "uk": "Парасолька", + "zh_Hans": "伞" + } + }, + { + "number": 38, + "emoji": "⌛", + "description": "Hourglass", + "unicode": "U+231B", + "translated_descriptions": { + "ar": "سَاعَةٌ رَملِيَّة", + "bg": "Пясъчен часовник", + "ca": "Rellotge de sorra", + "cs": "Přesýpací hodiny", + "de": "Sanduhr", + "eo": "Sablohorloĝo", + "es": "Reloj de arena", + "et": "Liivakell", + "fi": "Tiimalasi", + "fr": "Sablier", + "hr": "pješčani sat", + "hu": "Homokóra", + "it": "Clessidra", + "ja": "砂時計", + "nb_NO": "Timeglass", + "nl": "Zandloper", + "pt_BR": "Ampulheta", + "ru": "Песочные часы", + "si": null, + "sk": "Presýpacie hodiny", + "sr": "пешчаник", + "sv": "Timglas", + "szl": null, + "tzm": null, + "uk": "Пісковий годинник", + "zh_Hans": "沙漏" + } + }, + { + "number": 39, + "emoji": "⏰", + "description": "Clock", + "unicode": "U+23F0", + "translated_descriptions": { + "ar": "سَاعَة", + "bg": "Часовник", + "ca": "Rellotge", + "cs": "Hodiny", + "de": "Uhr", + "eo": "Horloĝo", + "es": "Reloj", + "et": "Kell", + "fi": "Pöytäkello", + "fr": "Réveil", + "hr": "sat", + "hu": "Óra", + "it": "Orologio", + "ja": "時計", + "nb_NO": "Klokke", + "nl": "Wekker", + "pt_BR": "Relógio", + "ru": "Часы", + "si": null, + "sk": "Budík", + "sr": "сат", + "sv": "Klocka", + "szl": null, + "tzm": null, + "uk": "Годинник", + "zh_Hans": "时钟" + } + }, + { + "number": 40, + "emoji": "🎁", + "description": "Gift", + "unicode": "U+1F381", + "translated_descriptions": { + "ar": "هَدِيَّة", + "bg": "Подарък", + "ca": "Regal", + "cs": "Dárek", + "de": "Geschenk", + "eo": "Donaco", + "es": "Regalo", + "et": "Kingitus", + "fi": "Lahja", + "fr": "Cadeau", + "hr": "poklon", + "hu": "Ajándék", + "it": "Regalo", + "ja": "ギフト", + "nb_NO": "Gave", + "nl": "Geschenk", + "pt_BR": "Presente", + "ru": "Подарок", + "si": null, + "sk": "Zabalený darček", + "sr": "поклон", + "sv": "Present", + "szl": null, + "tzm": null, + "uk": "Подарунок", + "zh_Hans": "礼物" + } + }, + { + "number": 41, + "emoji": "💡", + "description": "Light Bulb", + "unicode": "U+1F4A1", + "translated_descriptions": { + "ar": "مِصبَاح", + "bg": "Лампа", + "ca": "Bombeta", + "cs": "Žárovka", + "de": "Glühbirne", + "eo": "Lampo", + "es": "Bombilla", + "et": "Lambipirn", + "fi": "Hehkulamppu", + "fr": "Ampoule", + "hr": "žarulja", + "hu": "Égő", + "it": "Lampadina", + "ja": "電球", + "nb_NO": "Lyspære", + "nl": "Gloeilamp", + "pt_BR": "Lâmpada", + "ru": "Лампочка", + "si": null, + "sk": "Žiarovka", + "sr": "сијалица", + "sv": "Lampa", + "szl": null, + "tzm": null, + "uk": "Лампочка", + "zh_Hans": "灯泡" + } + }, + { + "number": 42, + "emoji": "📕", + "description": "Book", + "unicode": "U+1F4D5", + "translated_descriptions": { + "ar": "كِتَاب", + "bg": "Книга", + "ca": "Llibre", + "cs": "Kniha", + "de": "Buch", + "eo": "Libro", + "es": "Libro", + "et": "Raamat", + "fi": "Kirja", + "fr": "Livre", + "hr": "knjiga", + "hu": "Könyv", + "it": "Libro", + "ja": "本", + "nb_NO": "Bok", + "nl": "Boek", + "pt_BR": "Livro", + "ru": "Книга", + "si": null, + "sk": "Zatvorená kniha", + "sr": "књига", + "sv": "Bok", + "szl": null, + "tzm": "Adlis", + "uk": "Книга", + "zh_Hans": "书" + } + }, + { + "number": 43, + "emoji": "✏️", + "description": "Pencil", + "unicode": "U+270FU+FE0F", + "translated_descriptions": { + "ar": "قَلَمُ رَصاص", + "bg": "Молив", + "ca": "Llapis", + "cs": "Tužka", + "de": "Bleistift", + "eo": "Krajono", + "es": "Lápiz", + "et": "Pliiats", + "fi": "Lyijykynä", + "fr": "Crayon", + "hr": "olovka", + "hu": "Ceruza", + "it": "Matita", + "ja": "鉛筆", + "nb_NO": "Blyant", + "nl": "Potlood", + "pt_BR": "Lápis", + "ru": "Карандаш", + "si": null, + "sk": "Ceruzka", + "sr": "оловка", + "sv": "Penna", + "szl": null, + "tzm": null, + "uk": "Олівець", + "zh_Hans": "铅笔" + } + }, + { + "number": 44, + "emoji": "📎", + "description": "Paperclip", + "unicode": "U+1F4CE", + "translated_descriptions": { + "ar": "مِشبَكُ وَرَق", + "bg": "Кламер", + "ca": "Clip", + "cs": "Sponka", + "de": "Büroklammer", + "eo": "Paperkuntenilo", + "es": "Clip", + "et": "Kirjaklamber", + "fi": "Paperiliitin", + "fr": "Trombone", + "hr": "spajalica", + "hu": "Gémkapocs", + "it": "Graffetta", + "ja": "クリップ", + "nb_NO": "BInders", + "nl": "Papierklemmetje", + "pt_BR": "Clipe de papel", + "ru": "Скрепка", + "si": null, + "sk": "Sponka na papier", + "sr": "спајалица", + "sv": "Gem", + "szl": null, + "tzm": null, + "uk": "Спиначка", + "zh_Hans": "回形针" + } + }, + { + "number": 45, + "emoji": "✂️", + "description": "Scissors", + "unicode": "U+2702U+FE0F", + "translated_descriptions": { + "ar": "مِقَصّ", + "bg": "Ножици", + "ca": "Tisores", + "cs": "Nůžky", + "de": "Schere", + "eo": "Tondilo", + "es": "Tijeras", + "et": "Käärid", + "fi": "Sakset", + "fr": "Ciseaux", + "hr": "škare", + "hu": "Olló", + "it": "Forbici", + "ja": "はさみ", + "nb_NO": "Saks", + "nl": "Schaar", + "pt_BR": "Tesoura", + "ru": "Ножницы", + "si": null, + "sk": "Nožnice", + "sr": "маказе", + "sv": "Sax", + "szl": null, + "tzm": null, + "uk": "Ножиці", + "zh_Hans": "剪刀" + } + }, + { + "number": 46, + "emoji": "🔒", + "description": "Lock", + "unicode": "U+1F512", + "translated_descriptions": { + "ar": "قُفل", + "bg": "Катинар", + "ca": "Cadenat", + "cs": "Zámek", + "de": "Schloss", + "eo": "Seruro", + "es": "Candado", + "et": "Lukk", + "fi": "Lukko", + "fr": "Cadenas", + "hr": "zaključati", + "hu": "Lakat", + "it": "Lucchetto", + "ja": "錠前", + "nb_NO": "Lås", + "nl": "Slot", + "pt_BR": "Cadeado", + "ru": "Замок", + "si": null, + "sk": "Zatvorená zámka", + "sr": "катанац", + "sv": "Lås", + "szl": null, + "tzm": null, + "uk": "Замок", + "zh_Hans": "锁" + } + }, + { + "number": 47, + "emoji": "🔑", + "description": "Key", + "unicode": "U+1F511", + "translated_descriptions": { + "ar": "مِفتَاح", + "bg": "Ключ", + "ca": "Clau", + "cs": "Klíč", + "de": "Schlüssel", + "eo": "Ŝlosilo", + "es": "Llave", + "et": "Võti", + "fi": "Avain", + "fr": "Clé", + "hr": "ključ", + "hu": "Kulcs", + "it": "Chiave", + "ja": "鍵", + "nb_NO": "Nøkkel", + "nl": "Sleutel", + "pt_BR": "Chave", + "ru": "Ключ", + "si": null, + "sk": "Kľúč", + "sr": "кључ", + "sv": "Nyckel", + "szl": null, + "tzm": "Tasarut", + "uk": "Ключ", + "zh_Hans": "钥匙" + } + }, + { + "number": 48, + "emoji": "🔨", + "description": "Hammer", + "unicode": "U+1F528", + "translated_descriptions": { + "ar": "مِطرَقَة", + "bg": "Чук", + "ca": "Martell", + "cs": "Kladivo", + "de": "Hammer", + "eo": "Martelo", + "es": "Martillo", + "et": "Haamer", + "fi": "Vasara", + "fr": "Marteau", + "hr": "čekić", + "hu": "Kalapács", + "it": "Martello", + "ja": "金槌", + "nb_NO": "Hammer", + "nl": "Hamer", + "pt_BR": "Martelo", + "ru": "Молоток", + "si": null, + "sk": "Kladivo", + "sr": "чекић", + "sv": "Hammare", + "szl": null, + "tzm": null, + "uk": "Молоток", + "zh_Hans": "锤子" + } + }, + { + "number": 49, + "emoji": "☎️", + "description": "Telephone", + "unicode": "U+260EU+FE0F", + "translated_descriptions": { + "ar": "تِلِفُون", + "bg": "Телефон", + "ca": "Telèfon", + "cs": "Telefon", + "de": "Telefon", + "eo": "Telefono", + "es": "Telefono", + "et": "Telefon", + "fi": "Puhelin", + "fr": "Téléphone", + "hr": "telefon", + "hu": "Telefon", + "it": "Telefono", + "ja": "電話機", + "nb_NO": "Telefon", + "nl": "Telefoon", + "pt_BR": "Telefone", + "ru": "Телефон", + "si": null, + "sk": "Telefón", + "sr": "телефон", + "sv": "Telefon", + "szl": null, + "tzm": "Atilifun", + "uk": "Телефон", + "zh_Hans": "电话" + } + }, + { + "number": 50, + "emoji": "🏁", + "description": "Flag", + "unicode": "U+1F3C1", + "translated_descriptions": { + "ar": "عَلَم", + "bg": "Флаг", + "ca": "Bandera", + "cs": "Vlajka", + "de": "Flagge", + "eo": "Flago", + "es": "Bandera", + "et": "Lipp", + "fi": "Lippu", + "fr": "Drapeau", + "hr": "zastava", + "hu": "Zászló", + "it": "Bandiera", + "ja": "旗", + "nb_NO": "Flagg", + "nl": "Vlag", + "pt_BR": "Bandeira", + "ru": "Флаг", + "si": null, + "sk": "Kockovaná zástava", + "sr": "застава", + "sv": "Flagga", + "szl": null, + "tzm": "Acenyal", + "uk": "Прапор", + "zh_Hans": "旗帜" + } + }, + { + "number": 51, + "emoji": "🚂", + "description": "Train", + "unicode": "U+1F682", + "translated_descriptions": { + "ar": "قِطَار", + "bg": "Влак", + "ca": "Tren", + "cs": "Vlak", + "de": "Zug", + "eo": "Vagonaro", + "es": "Tren", + "et": "Rong", + "fi": "Juna", + "fr": "Train", + "hr": "vlak", + "hu": "Vonat", + "it": "Treno", + "ja": "電車", + "nb_NO": "Tog", + "nl": "Trein", + "pt_BR": "Trem", + "ru": "Поезд", + "si": null, + "sk": "Rušeň", + "sr": "воз", + "sv": "Tåg", + "szl": null, + "tzm": null, + "uk": "Потяг", + "zh_Hans": "火车" + } + }, + { + "number": 52, + "emoji": "🚲", + "description": "Bicycle", + "unicode": "U+1F6B2", + "translated_descriptions": { + "ar": "دَرّاجَة", + "bg": "Колело", + "ca": "Bicicleta", + "cs": "Kolo", + "de": "Fahrrad", + "eo": "Biciklo", + "es": "Bicicleta", + "et": "Jalgratas", + "fi": "Polkupyörä", + "fr": "Vélo", + "hr": "bicikl", + "hu": "Kerékpár", + "it": "Bicicletta", + "ja": "自転車", + "nb_NO": "Sykkel", + "nl": "Fiets", + "pt_BR": "Bicicleta", + "ru": "Велосипед", + "si": null, + "sk": "Bicykel", + "sr": "бицикл", + "sv": "Cykel", + "szl": null, + "tzm": null, + "uk": "Велосипед", + "zh_Hans": "自行车" + } + }, + { + "number": 53, + "emoji": "✈️", + "description": "Aeroplane", + "unicode": "U+2708U+FE0F", + "translated_descriptions": { + "ar": "طَائِرة", + "bg": "Самолет", + "ca": "Avió", + "cs": "Letadlo", + "de": "Flugzeug", + "eo": "Aviadilo", + "es": "Avión", + "et": "Lennuk", + "fi": "Lentokone", + "fr": "Avion", + "hr": "avion", + "hu": "Repülő", + "it": "Aeroplano", + "ja": "飛行機", + "nb_NO": "Fly", + "nl": "Vliegtuig", + "pt_BR": "Avião", + "ru": "Самолет", + "si": null, + "sk": "Lietadlo", + "sr": "авион", + "sv": "Flygplan", + "szl": null, + "tzm": null, + "uk": "Літак", + "zh_Hans": "飞机" + } + }, + { + "number": 54, + "emoji": "🚀", + "description": "Rocket", + "unicode": "U+1F680", + "translated_descriptions": { + "ar": "صَارُوخ", + "bg": "Ракета", + "ca": "Coet", + "cs": "Raketa", + "de": "Rakete", + "eo": "Raketo", + "es": "Cohete", + "et": "Rakett", + "fi": "Raketti", + "fr": "Fusée", + "hr": "raketa", + "hu": "Rakáta", + "it": "Razzo", + "ja": "ロケット", + "nb_NO": "Rakett", + "nl": "Raket", + "pt_BR": "Foguete", + "ru": "Ракета", + "si": null, + "sk": "Raketa", + "sr": "ракета", + "sv": "Raket", + "szl": null, + "tzm": null, + "uk": "Ракета", + "zh_Hans": "火箭" + } + }, + { + "number": 55, + "emoji": "🏆", + "description": "Trophy", + "unicode": "U+1F3C6", + "translated_descriptions": { + "ar": "كَأسُ النَّصر", + "bg": "Трофей", + "ca": "Trofeu", + "cs": "Pohár", + "de": "Pokal", + "eo": "Trofeo", + "es": "Trofeo", + "et": "Auhind", + "fi": "Palkinto", + "fr": "Trophée", + "hr": "trofej", + "hu": "Trófea", + "it": "Trofeo", + "ja": "トロフィー", + "nb_NO": "Pokal", + "nl": "Trofee", + "pt_BR": "Troféu", + "ru": "Кубок", + "si": null, + "sk": "Trofej", + "sr": "пехар", + "sv": "Trofé", + "szl": null, + "tzm": null, + "uk": "Приз", + "zh_Hans": "奖杯" + } + }, + { + "number": 56, + "emoji": "⚽", + "description": "Ball", + "unicode": "U+26BD", + "translated_descriptions": { + "ar": "كُرَة", + "bg": "Топка", + "ca": "Pilota", + "cs": "Míč", + "de": "Ball", + "eo": "Pilko", + "es": "Bola", + "et": "Pall", + "fi": "Pallo", + "fr": "Ballon", + "hr": "lopta", + "hu": "Labda", + "it": "Palla", + "ja": "ボール", + "nb_NO": "Ball", + "nl": "Bal", + "pt_BR": "Bola", + "ru": "Мяч", + "si": null, + "sk": "Futbal", + "sr": "лопта", + "sv": "Boll", + "szl": null, + "tzm": "Tcama", + "uk": "М'яч", + "zh_Hans": "球" + } + }, + { + "number": 57, + "emoji": "🎸", + "description": "Guitar", + "unicode": "U+1F3B8", + "translated_descriptions": { + "ar": "غيتار", + "bg": "Китара", + "ca": "Guitarra", + "cs": "Kytara", + "de": "Gitarre", + "eo": "Gitaro", + "es": "Guitarra", + "et": "Kitarr", + "fi": "Kitara", + "fr": "Guitare", + "hr": "gitara", + "hu": "Gitár", + "it": "Chitarra", + "ja": "ギター", + "nb_NO": "Gitar", + "nl": "Gitaar", + "pt_BR": "Guitarra", + "ru": "Гитара", + "si": null, + "sk": "Gitara", + "sr": "гитара", + "sv": "Gitarr", + "szl": null, + "tzm": "Agiṭaṛ", + "uk": "Гітара", + "zh_Hans": "吉他" + } + }, + { + "number": 58, + "emoji": "🎺", + "description": "Trumpet", + "unicode": "U+1F3BA", + "translated_descriptions": { + "ar": "بُوق", + "bg": "Тромпет", + "ca": "Trompeta", + "cs": "Trumpeta", + "de": "Trompete", + "eo": "Trumpeto", + "es": "Trompeta", + "et": "Trompet", + "fi": "Trumpetti", + "fr": "Trompette", + "hr": "truba", + "hu": "Trombita", + "it": "Trombetta", + "ja": "トランペット", + "nb_NO": "Trompet", + "nl": "Trompet", + "pt_BR": "Trombeta", + "ru": "Труба", + "si": null, + "sk": "Trúbka", + "sr": "труба", + "sv": "Trumpet", + "szl": null, + "tzm": null, + "uk": "Труба", + "zh_Hans": "喇叭" + } + }, + { + "number": 59, + "emoji": "🔔", + "description": "Bell", + "unicode": "U+1F514", + "translated_descriptions": { + "ar": "جَرَس", + "bg": "Звънец", + "ca": "Campana", + "cs": "Zvonek", + "de": "Glocke", + "eo": "Sonorilo", + "es": "Campana", + "et": "Kelluke", + "fi": "Soittokello", + "fr": "Cloche", + "hr": "zvono", + "hu": "Harang", + "it": "Campana", + "ja": "ベル", + "nb_NO": "Bjelle", + "nl": "Bel", + "pt_BR": "Sino", + "ru": "Колокол", + "si": null, + "sk": "Zvon", + "sr": "звоно", + "sv": "Bjällra", + "szl": null, + "tzm": null, + "uk": "Дзвін", + "zh_Hans": "铃铛" + } + }, + { + "number": 60, + "emoji": "⚓", + "description": "Anchor", + "unicode": "U+2693", + "translated_descriptions": { + "ar": "مِرسَاة", + "bg": "Котва", + "ca": "Àncora", + "cs": "Kotva", + "de": "Anker", + "eo": "Ankro", + "es": "Ancla", + "et": "Ankur", + "fi": "Ankkuri", + "fr": "Ancre", + "hr": "sidro", + "hu": "Horgony", + "it": "Ancora", + "ja": "いかり", + "nb_NO": "Anker", + "nl": "Anker", + "pt_BR": "Âncora", + "ru": "Якорь", + "si": null, + "sk": "Kotva", + "sr": "сидро", + "sv": "Ankare", + "szl": null, + "tzm": null, + "uk": "Якір", + "zh_Hans": "锚" + } + }, + { + "number": 61, + "emoji": "🎧", + "description": "Headphones", + "unicode": "U+1F3A7", + "translated_descriptions": { + "ar": "سَمّاعَة رَأس", + "bg": "Слушалки", + "ca": "Auriculars", + "cs": "Sluchátka", + "de": "Kopfhörer", + "eo": "Kapaŭdilo", + "es": "Cascos", + "et": "Kõrvaklapid", + "fi": "Kuulokkeet", + "fr": "Casque audio", + "hr": "slušalice", + "hu": "Fejhallgató", + "it": "Cuffie", + "ja": "ヘッドホン", + "nb_NO": "Hodetelefoner", + "nl": "Koptelefoon", + "pt_BR": "Fones de ouvido", + "ru": "Наушники", + "si": null, + "sk": "Slúchadlá", + "sr": "слушалице", + "sv": "Hörlurar", + "szl": null, + "tzm": null, + "uk": "Навушники", + "zh_Hans": "耳机" + } + }, + { + "number": 62, + "emoji": "📁", + "description": "Folder", + "unicode": "U+1F4C1", + "translated_descriptions": { + "ar": "مُجَلَّد", + "bg": "Папка", + "ca": "Carpeta", + "cs": "Složka", + "de": "Ordner", + "eo": "Dosierujo", + "es": "Carpeta", + "et": "Kaust", + "fi": "Kansio", + "fr": "Dossier", + "hr": "mapu", + "hu": "Mappa", + "it": "Cartella", + "ja": "フォルダ", + "nb_NO": "Mappe", + "nl": "Map", + "pt_BR": "Pasta", + "ru": "Папка", + "si": null, + "sk": "Fascikel", + "sr": "фасцикла", + "sv": "Mapp", + "szl": null, + "tzm": "Asdaw", + "uk": "Тека", + "zh_Hans": "文件夹" + } + }, + { + "number": 63, + "emoji": "📌", + "description": "Pin", + "unicode": "U+1F4CC", + "translated_descriptions": { + "ar": "دَبُّوس", + "bg": "Кабърче", + "ca": "Xinxeta", + "cs": "Špendlík", + "de": "Stecknadel", + "eo": "Pinglo", + "es": "Alfiler", + "et": "Nööpnõel", + "fi": "Nuppineula", + "fr": "Punaise", + "hr": "pribadača", + "hu": "Rajszeg", + "it": "Puntina", + "ja": "ピン", + "nb_NO": "Tegnestift", + "nl": "Duimspijker", + "pt_BR": "Alfinete", + "ru": "Булавка", + "si": null, + "sk": "Špendlík", + "sr": "чиода", + "sv": "Häftstift", + "szl": null, + "tzm": null, + "uk": "Кнопка", + "zh_Hans": "图钉" + } + } +] -- cgit v1.2.3 From 72ffe9651fcdc3dfc0ceb34bd677aa15bbbe2ebf Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 9 Feb 2022 11:23:46 +0100 Subject: CMakeLists.txt: add roomcanonicalaliasevent.h Yet another missing header from times when .h files weren't added to CMakeLists. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 15726240..dc2459e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ list(APPEND lib_SRCS lib/events/roomtombstoneevent.h lib/events/roomtombstoneevent.cpp lib/events/roommessageevent.h lib/events/roommessageevent.cpp lib/events/roommemberevent.h lib/events/roommemberevent.cpp + lib/events/roomcanonicalaliasevent.h lib/events/roomavatarevent.h lib/events/roompowerlevelsevent.h lib/events/roompowerlevelsevent.cpp lib/events/typingevent.h lib/events/typingevent.cpp -- cgit v1.2.3 From 272cb01b05529971ea38e09bf75d8d8f194a9dd8 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 5 May 2022 14:23:14 +0200 Subject: Fix license identifier --- lib/events/encryptedfile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 0558563f..d0c4a030 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan // -// SPDX-License-Identifier: LGPl-2.1-or-later +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -- cgit v1.2.3 From b79f67919e05698a8c3daffbe0fe53fa1ce46e54 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 8 May 2022 17:43:08 +0200 Subject: toSnakeCase and EventContent::SingleKeyValue This is a rework of EventContent::SimpleContent previously defined in simplestateevents.h. Quite a few events (and not only state events) have just a single key-value pair in their content - this structure (which is really just a template wrapper around the value) and the accompanying JsonConverter<> specialisation encapsulate the concept to streamline definition of such events. This commit only has simplestateevents.h using it; further commits will use SingleKeyValue in other places. toSnakeCase is a facility function that converts camelCase used for C++ variables into snake_case used in JSON payloads. Combined with the preprocessor trick that makes a string literal from an identifier, this allows to reduce boilerplate code that repeats the same name for fields in C++ event classes and fields in JSON. SingleKeyValue uses it, and there are other cases for it coming. --- CMakeLists.txt | 1 + lib/converters.h | 16 ++++++++++ lib/events/simplestateevents.h | 72 ++++++++++++++++-------------------------- lib/events/single_key_value.h | 27 ++++++++++++++++ 4 files changed, 72 insertions(+), 44 deletions(-) create mode 100644 lib/events/single_key_value.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dc2459e9..537d580e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ list(APPEND lib_SRCS lib/events/eventloader.h lib/events/roomevent.h lib/events/roomevent.cpp lib/events/stateevent.h lib/events/stateevent.cpp + lib/events/single_key_value.h lib/events/simplestateevents.h lib/events/eventcontent.h lib/events/eventcontent.cpp lib/events/eventrelation.h lib/events/eventrelation.cpp diff --git a/lib/converters.h b/lib/converters.h index 515c96fd..6515310a 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -414,4 +414,20 @@ inline void addParam(ContT& container, const QString& key, ValT&& value) _impl::AddNode, Force>::impl(container, key, std::forward(value)); } + +// This is a facility function to convert camelCase method/variable names +// used throughout Quotient to snake_case JSON keys - see usage in +// single_key_value.h and event.h (DEFINE_CONTENT_GETTER macro). +inline auto toSnakeCase(QLatin1String s) +{ + QString result { s }; + for (auto it = result.begin(); it != result.end(); ++it) + if (it->isUpper()) { + const auto offset = static_cast(it - result.begin()); + result.insert(offset, '_'); // NB: invalidates iterators + it = result.begin() + offset + 1; + *it = it->toLower(); + } + return result; +} } // namespace Quotient diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 9610574b..33221542 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -4,63 +4,47 @@ #pragma once #include "stateevent.h" +#include "single_key_value.h" namespace Quotient { -namespace EventContent { - template - struct SimpleContent { - using value_type = T; - - // The constructor is templated to enable perfect forwarding - template - SimpleContent(QString keyName, TT&& value) - : value(std::forward(value)), key(std::move(keyName)) - {} - SimpleContent(const QJsonObject& json, QString keyName) - : value(fromJson(json[keyName])), key(std::move(keyName)) - {} - QJsonObject toJson() const - { - return { { key, Quotient::toJson(value) } }; - } - - T value; - const QString key; - }; -} // namespace EventContent - -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - class QUOTIENT_API _Name \ - : public StateEvent> { \ - public: \ - using value_type = content_type::value_type; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - template \ - explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), QString(), \ - QStringLiteral(#_ContentKey), std::forward(value)) \ - {} \ - explicit _Name(QJsonObject obj) \ - : StateEvent(typeId(), std::move(obj), \ - QStringLiteral(#_ContentKey)) \ - {} \ - auto _ContentKey() const { return content().value; } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + constexpr auto _Name##Key = #_ContentKey##_ls; \ + class QUOTIENT_API _Name \ + : public StateEvent< \ + EventContent::SingleKeyValue<_ValueType, &_Name##Key>> { \ + public: \ + using value_type = _ValueType; \ + DEFINE_EVENT_TYPEID(_TypeId, _Name) \ + template \ + explicit _Name(T&& value) \ + : StateEvent(TypeId, matrixTypeId(), QString(), \ + std::forward(value)) \ + {} \ + explicit _Name(QJsonObject obj) \ + : StateEvent(TypeId, std::move(obj)) \ + {} \ + auto _ContentKey() const { return content().value; } \ + }; \ + REGISTER_EVENT_TYPE(_Name) \ // End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) -DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) +DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", + QStringList, pinnedEvents) +constexpr auto RoomAliasesEventKey = "aliases"_ls; class [[deprecated( "m.room.aliases events are deprecated by the Matrix spec; use" " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // -RoomAliasesEvent : public StateEvent> { +RoomAliasesEvent + : public StateEvent< + EventContent::SingleKeyValue> +{ public: DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) explicit RoomAliasesEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj, QStringLiteral("aliases")) + : StateEvent(typeId(), obj) {} QString server() const { return stateKey(); } QStringList aliases() const { return content().value; } diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h new file mode 100644 index 00000000..75ca8cd2 --- /dev/null +++ b/lib/events/single_key_value.h @@ -0,0 +1,27 @@ +#pragma once + +#include "converters.h" + +namespace Quotient { + +namespace EventContent { + template + struct SingleKeyValue { + T value; + }; +} // namespace EventContent + +template +struct JsonConverter> { + using content_type = EventContent::SingleKeyValue; + static content_type load(const QJsonValue& jv) + { + return { fromJson(jv.toObject().value(JsonKey)) }; + } + static QJsonObject dump(const content_type& c) + { + return { { JsonKey, toJson(c.value) } }; + } + static inline const auto JsonKey = toSnakeCase(*KeyStr); +}; +} // namespace Quotient -- cgit v1.2.3 From b094a75f704b59f1382b6eec1f3bf0ab51fb0a85 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 4 Jan 2022 10:30:17 +0100 Subject: StateEvent: use non-member JSON converters With the reworked JsonConverter code it is possible to work uniformly with structures that have a member toJson() and a constructor converting from QJsonObject, as well as with structures that rely on an external JsonConverter specialisation. --- lib/events/eventcontent.h | 1 - lib/events/stateevent.h | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index de9a792b..76f20f79 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -34,7 +34,6 @@ namespace EventContent { explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} virtual ~Base() = default; - // FIXME: make toJson() from converters.* work on base classes QJsonObject toJson() const; public: diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 88da68f8..19905431 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -72,7 +72,7 @@ struct Prev { explicit Prev(const QJsonObject& unsignedJson, ContentParamTs&&... contentParams) : senderId(unsignedJson.value("prev_sender"_ls).toString()) - , content(unsignedJson.value(PrevContentKeyL).toObject(), + , content(fromJson(unsignedJson.value(PrevContentKeyL)), std::forward(contentParams)...) {} @@ -89,7 +89,8 @@ public: explicit StateEvent(Type type, const QJsonObject& fullJson, ContentParamTs&&... contentParams) : StateEventBase(type, fullJson) - , _content(contentJson(), std::forward(contentParams)...) + , _content(fromJson(contentJson()), + std::forward(contentParams)...) { const auto& unsignedData = unsignedJson(); if (unsignedData.contains(PrevContentKeyL)) @@ -101,9 +102,9 @@ public: const QString& stateKey, ContentParamTs&&... contentParams) : StateEventBase(type, matrixType, stateKey) - , _content(std::forward(contentParams)...) + , _content{std::forward(contentParams)...} { - editJson().insert(ContentKey, _content.toJson()); + editJson().insert(ContentKey, toJson(_content)); } const ContentT& content() const { return _content; } @@ -111,7 +112,7 @@ public: void editContent(VisitorT&& visitor) { visitor(_content); - editJson()[ContentKeyL] = _content.toJson(); + editJson()[ContentKeyL] = toJson(_content); } const ContentT* prevContent() const { -- cgit v1.2.3 From 09ecce1744344f1bfbacd31c8f300440a4139be4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 4 Jan 2022 12:46:34 +0100 Subject: Cleanup; comments reformatting --- lib/events/eventcontent.cpp | 24 +++--- lib/events/eventcontent.h | 198 +++++++++++++++++++++----------------------- 2 files changed, 109 insertions(+), 113 deletions(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 9d7edf20..479d5da0 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -29,12 +29,13 @@ FileInfo::FileInfo(const QFileInfo &fi) } FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, - Omittable file, QString originalFilename) + Omittable encryptedFile, + QString originalFilename) : mimeType(mimeType) , url(move(u)) , payloadSize(payloadSize) , originalName(move(originalFilename)) - , file(file) + , file(move(encryptedFile)) { if (!isValid()) qCWarning(MESSAGES) @@ -44,7 +45,7 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, } FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - const Omittable &file, + Omittable encryptedFile, QString originalFilename) : originalInfoJson(infoJson) , mimeType( @@ -52,7 +53,7 @@ FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, , url(move(mxcUrl)) , payloadSize(fromJson(infoJson["size"_ls])) , originalName(move(originalFilename)) - , file(file) + , file(move(encryptedFile)) { if(url.isEmpty() && file.has_value()) { url = file->url; @@ -82,15 +83,16 @@ ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) {} ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, - QSize imageSize, const Omittable &file, const QString& originalFilename) - : FileInfo(mxcUrl, fileSize, type, file, originalFilename) + QSize imageSize, Omittable encryptedFile, + const QString& originalFilename) + : FileInfo(mxcUrl, fileSize, type, move(encryptedFile), originalFilename) , imageSize(imageSize) {} ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - const Omittable &file, + Omittable encryptedFile, const QString& originalFilename) - : FileInfo(mxcUrl, infoJson, file, originalFilename) + : FileInfo(mxcUrl, infoJson, move(encryptedFile), originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} @@ -103,10 +105,10 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("h"), imageSize.height()); } -Thumbnail::Thumbnail(const QJsonObject& infoJson, const Omittable &file) +Thumbnail::Thumbnail(const QJsonObject& infoJson, + Omittable encryptedFile) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), - infoJson["thumbnail_info"_ls].toObject(), - file) + infoJson["thumbnail_info"_ls].toObject(), move(encryptedFile)) {} void Thumbnail::fillInfoJson(QJsonObject* infoJson) const diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 76f20f79..4134202a 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -19,16 +19,13 @@ class QFileInfo; namespace Quotient { namespace EventContent { - /** - * A base class for all content types that can be stored - * in a RoomMessageEvent - * - * Each content type class should have a constructor taking - * a QJsonObject and override fillJson() with an implementation - * that will fill the target QJsonObject with stored values. It is - * assumed but not required that a content object can also be created - * from plain data. - */ + //! \brief Base for all content types that can be stored in RoomMessageEvent + //! + //! Each content type class should have a constructor taking + //! a QJsonObject and override fillJson() with an implementation + //! that will fill the target QJsonObject with stored values. It is + //! assumed but not required that a content object can also be created + //! from plain data. class QUOTIENT_API Base { public: explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} @@ -60,45 +57,55 @@ namespace EventContent { // ImageContent : UrlWithThumbnailContent // VideoContent : PlayableContent> - /** - * A base/mixin class for structures representing an "info" object for - * some content types. These include most attachment types currently in - * the CS API spec. - * - * In order to use it in a content class, derive both from TypedBase - * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) - * and call fillInfoJson() to fill the "info" subobject. Make sure - * to pass an "info" part of JSON to FileInfo constructor, not the whole - * JSON content, as well as contents of "url" (or a similar key) and - * optionally "filename" node from the main JSON content. Assuming you - * don't do unusual things, you should use \p UrlBasedContent<> instead - * of doing multiple inheritance and overriding Base::fillJson() by hand. - * - * This class is not polymorphic. - */ + //! \brief Mix-in class representing `info` subobject in content JSON + //! + //! This is one of base classes for content types that deal with files or + //! URLs. It stores the file metadata attributes, such as size, MIME type + //! etc. found in the `content/info` subobject of event JSON payloads. + //! Actual content classes derive from this class _and_ TypedBase that + //! provides a polymorphic interface to access data in the mix-in. FileInfo + //! (as well as ImageInfo, that adds image size to the metadata) is NOT + //! polymorphic and is used in a non-polymorphic way to store thumbnail + //! metadata (in a separate instance), next to the metadata on the file + //! itself. + //! + //! If you need to make a new _content_ (not info) class based on files/URLs + //! take UrlBasedContent as the example, i.e.: + //! 1. Double-inherit from this class (or ImageInfo) and TypedBase. + //! 2. Provide a constructor from QJsonObject that will pass the `info` + //! subobject (not the whole content JSON) down to FileInfo/ImageInfo. + //! 3. Override fillJson() to customise the JSON export logic. Make sure + //! to call toInfoJson() from it to produce the payload for the `info` + //! subobject in the JSON payload. + //! + //! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent, + //! UrlBasedContent class QUOTIENT_API FileInfo { public: FileInfo() = default; + //! \brief Construct from a QFileInfo object + //! + //! \param fi a QFileInfo object referring to an existing file explicit FileInfo(const QFileInfo& fi); explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, const QMimeType& mimeType = {}, - Omittable file = none, + Omittable encryptedFile = none, QString originalFilename = {}); + //! \brief Construct from a JSON `info` payload + //! + //! Make sure to pass the `info` subobject of content JSON, not the + //! whole JSON content. FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - const Omittable &file, + Omittable encryptedFile, QString originalFilename = {}); bool isValid() const; - void fillInfoJson(QJsonObject* infoJson) const; - - /** - * \brief Extract media id from the URL - * - * This can be used, e.g., to construct a QML-facing image:// - * URI as follows: - * \code "image://provider/" + info.mediaId() \endcode - */ + //! \brief Extract media id from the URL + //! + //! This can be used, e.g., to construct a QML-facing image:// + //! URI as follows: + //! \code "image://provider/" + info.mediaId() \endcode QString mediaId() const { return url.authority() + url.path(); } public: @@ -118,19 +125,17 @@ namespace EventContent { return infoJson; } - /** - * A content info class for image content types: image, thumbnail, video - */ + //! \brief A content info class for image/video content types and thumbnails class QUOTIENT_API ImageInfo : public FileInfo { public: ImageInfo() = default; explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, const QMimeType& type = {}, QSize imageSize = {}, - const Omittable &file = none, + Omittable encryptedFile = none, const QString& originalFilename = {}); ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - const Omittable &encryptedFile, + Omittable encryptedFile, const QString& originalFilename = {}); void fillInfoJson(QJsonObject* infoJson) const; @@ -139,22 +144,18 @@ namespace EventContent { QSize imageSize; }; - /** - * An auxiliary class for an info type that carries a thumbnail - * - * This class saves/loads a thumbnail to/from "info" subobject of - * the JSON representation of event content; namely, - * "info/thumbnail_url" and "info/thumbnail_info" fields are used. - */ + //! \brief An auxiliary class for an info type that carries a thumbnail + //! + //! This class saves/loads a thumbnail to/from `info` subobject of + //! the JSON representation of event content; namely, `info/thumbnail_url` + //! and `info/thumbnail_info` fields are used. class QUOTIENT_API Thumbnail : public ImageInfo { public: using ImageInfo::ImageInfo; - Thumbnail(const QJsonObject& infoJson, const Omittable &file = none); + Thumbnail(const QJsonObject& infoJson, + Omittable encryptedFile = none); - /** - * Writes thumbnail information to "thumbnail_info" subobject - * and thumbnail URL to "thumbnail_url" node inside "info". - */ + //! \brief Add thumbnail information to the passed `info` JSON object void fillInfoJson(QJsonObject* infoJson) const; }; @@ -170,18 +171,15 @@ namespace EventContent { using Base::Base; }; - /** - * A base class for content types that have a URL and additional info - * - * Types that derive from this class template take "url" and, - * optionally, "filename" values from the top-level JSON object and - * the rest of information from the "info" subobject, as defined by - * the parameter type. - * - * \tparam InfoT base info class - */ + //! \brief A template class for content types with a URL and additional info + //! + //! Types that derive from this class template take `url` and, + //! optionally, `filename` values from the top-level JSON object and + //! the rest of information from the `info` subobject, as defined by + //! the parameter type. + //! \tparam InfoT base info class - FileInfo or ImageInfo template - class QUOTIENT_API UrlBasedContent : public TypedBase, public InfoT { + class UrlBasedContent : public TypedBase, public InfoT { public: using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) @@ -241,43 +239,39 @@ namespace EventContent { } }; - /** - * Content class for m.image - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for the main image: - * - payloadSize - * - mimeType - * - imageSize - */ - using ImageContent = UrlWithThumbnailContent; - - /** - * Content class for m.file - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize (QSize for "h" and "w" in JSON) - */ - using FileContent = UrlWithThumbnailContent; + //! \brief Content class for m.image + //! + //! Available fields: + //! - corresponding to the top-level JSON: + //! - url + //! - filename (extension to the spec) + //! - corresponding to the `info` subobject: + //! - payloadSize (`size` in JSON) + //! - mimeType (`mimetype` in JSON) + //! - imageSize (QSize for a combination of `h` and `w` in JSON) + //! - thumbnail.url (`thumbnail_url` in JSON) + //! - corresponding to the `info/thumbnail_info` subobject: contents of + //! thumbnail field, in the same vein as for the main image: + //! - payloadSize + //! - mimeType + //! - imageSize + using ImageContent = UrlBasedContent; + + //! \brief Content class for m.file + //! + //! Available fields: + //! - corresponding to the top-level JSON: + //! - url + //! - filename + //! - corresponding to the `info` subobject: + //! - payloadSize (`size` in JSON) + //! - mimeType (`mimetype` in JSON) + //! - thumbnail.url (`thumbnail_url` in JSON) + //! - corresponding to the `info/thumbnail_info` subobject: + //! - thumbnail.payloadSize + //! - thumbnail.mimeType + //! - thumbnail.imageSize (QSize for `h` and `w` in JSON) + using FileContent = UrlBasedContent; } // namespace EventContent } // namespace Quotient Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) -- cgit v1.2.3 From d271a171dbec4068e43e8f711d5d2e966a2072ac Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 4 Jan 2022 15:25:49 +0100 Subject: Simplify EventContent a bit Main changes: 1. Base::fillJson() gets a QJsonObject& instead of QJsonObject* - c'mon, there's nothing inherently wrong with using an lvalue reference for a read-write parameter. 2. UrlWithThumbnailContent merged into UrlBasedContent. The original UrlBasedContent was only used to produce a single class, AudioContent, and even that can logically have a thumbnail even if the spec doesn't provision that. And there's no guarantee even for visual content (ImageContent, e.g.) to have thumbnail data; the fallback is already tested. 3. toInfoJson is converted from a template to a couple of overloads that supersede fillInfoJson() member functions in FileInfo/ImageInfo. These overloads are easier on the eye; and clang-tidy no more warns about ImageInfo::fillInfoJson() shadowing FileInfo::fillInfoJson(). 4. Now that UrlWithThumbnail is gone, PlayableContent can directly derive from UrlBasedContent since both its specialisations use it. 5. Instead of FileInfo/ImageInfo, fillInfoJson() has been reinvented within UrlBasedContent so that, in particular, PlayableContent wouldn't need to extract 'info' subobject and then roll it back inside the content JSON object. --- lib/events/encryptionevent.cpp | 9 ++-- lib/events/encryptionevent.h | 2 +- lib/events/eventcontent.cpp | 36 ++++++++-------- lib/events/eventcontent.h | 84 ++++++++++++++----------------------- lib/events/roommemberevent.cpp | 11 +++-- lib/events/roommemberevent.h | 2 +- lib/events/roommessageevent.cpp | 18 ++++---- lib/events/roommessageevent.h | 29 +++++++------ lib/events/roompowerlevelsevent.cpp | 22 +++++----- lib/events/roompowerlevelsevent.h | 2 +- 10 files changed, 98 insertions(+), 117 deletions(-) diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 6272c668..47b0a032 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -47,11 +47,10 @@ EncryptionEventContent::EncryptionEventContent(EncryptionType et) } } -void EncryptionEventContent::fillJson(QJsonObject* o) const +void EncryptionEventContent::fillJson(QJsonObject& o) const { - Q_ASSERT(o); if (encryption != EncryptionType::Undefined) - o->insert(AlgorithmKey, algorithm); - o->insert(RotationPeriodMsKey, rotationPeriodMs); - o->insert(RotationPeriodMsgsKey, rotationPeriodMsgs); + o.insert(AlgorithmKey, algorithm); + o.insert(RotationPeriodMsKey, rotationPeriodMs); + o.insert(RotationPeriodMsgsKey, rotationPeriodMsgs); } diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 124ced33..fe76b1af 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -26,7 +26,7 @@ public: int rotationPeriodMsgs; protected: - void fillJson(QJsonObject* o) const override; + void fillJson(QJsonObject& o) const override; }; using EncryptionType = EncryptionEventContent::EncryptionType; diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 479d5da0..6218e3b8 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -15,7 +15,7 @@ using std::move; QJsonObject Base::toJson() const { QJsonObject o; - fillJson(&o); + fillJson(o); return o; } @@ -68,14 +68,15 @@ bool FileInfo::isValid() const && (url.authority() + url.path()).count('/') == 1; } -void FileInfo::fillInfoJson(QJsonObject* infoJson) const +QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) { - Q_ASSERT(infoJson); - if (payloadSize != -1) - infoJson->insert(QStringLiteral("size"), payloadSize); - if (mimeType.isValid()) - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + QJsonObject infoJson; + if (info.payloadSize != -1) + infoJson.insert(QStringLiteral("size"), info.payloadSize); + if (info.mimeType.isValid()) + infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name()); //TODO add encryptedfile + return infoJson; } ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) @@ -96,13 +97,14 @@ ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} -void ImageInfo::fillInfoJson(QJsonObject* infoJson) const +QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) { - FileInfo::fillInfoJson(infoJson); - if (imageSize.width() != -1) - infoJson->insert(QStringLiteral("w"), imageSize.width()); - if (imageSize.height() != -1) - infoJson->insert(QStringLiteral("h"), imageSize.height()); + auto infoJson = toInfoJson(static_cast(info)); + if (info.imageSize.width() != -1) + infoJson.insert(QStringLiteral("w"), info.imageSize.width()); + if (info.imageSize.height() != -1) + infoJson.insert(QStringLiteral("h"), info.imageSize.height()); + return infoJson; } Thumbnail::Thumbnail(const QJsonObject& infoJson, @@ -111,11 +113,11 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson, infoJson["thumbnail_info"_ls].toObject(), move(encryptedFile)) {} -void Thumbnail::fillInfoJson(QJsonObject* infoJson) const +void Thumbnail::dumpTo(QJsonObject& infoJson) const { if (url.isValid()) - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); + infoJson.insert(QStringLiteral("thumbnail_url"), url.toString()); if (!imageSize.isEmpty()) - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson(*this)); + infoJson.insert(QStringLiteral("thumbnail_info"), + toInfoJson(*this)); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 4134202a..6aee333d 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -40,7 +40,7 @@ namespace EventContent { Base(const Base&) = default; Base(Base&&) = default; - virtual void fillJson(QJsonObject* o) const = 0; + virtual void fillJson(QJsonObject& o) const = 0; }; // The below structures fairly follow CS spec 11.2.1.6. The overall @@ -50,12 +50,14 @@ namespace EventContent { // A quick classes inheritance structure follows (the definitions are // spread across eventcontent.h and roommessageevent.h): + // UrlBasedContent : InfoT + url and thumbnail data + // PlayableContent : + duration attribute // FileInfo - // FileContent : UrlWithThumbnailContent - // AudioContent : PlayableContent> + // FileContent = UrlBasedContent + // AudioContent = PlayableContent // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlWithThumbnailContent - // VideoContent : PlayableContent> + // ImageContent = UrlBasedContent + // VideoContent = PlayableContent //! \brief Mix-in class representing `info` subobject in content JSON //! @@ -117,13 +119,7 @@ namespace EventContent { Omittable file = none; }; - template - QJsonObject toInfoJson(const InfoT& info) - { - QJsonObject infoJson; - info.fillInfoJson(&infoJson); - return infoJson; - } + QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); //! \brief A content info class for image/video content types and thumbnails class QUOTIENT_API ImageInfo : public FileInfo { @@ -138,12 +134,12 @@ namespace EventContent { Omittable encryptedFile, const QString& originalFilename = {}); - void fillInfoJson(QJsonObject* infoJson) const; - public: QSize imageSize; }; + QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info); + //! \brief An auxiliary class for an info type that carries a thumbnail //! //! This class saves/loads a thumbnail to/from `info` subobject of @@ -156,7 +152,7 @@ namespace EventContent { Omittable encryptedFile = none); //! \brief Add thumbnail information to the passed `info` JSON object - void fillInfoJson(QJsonObject* infoJson) const; + void dumpTo(QJsonObject& infoJson) const; }; class QUOTIENT_API TypedBase : public Base { @@ -185,57 +181,41 @@ namespace EventContent { explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - fromJson>(json["file"]), json["filename"].toString()) + fromJson>(json["file"]), + json["filename"].toString()) + , thumbnail(FileInfo::originalInfoJson) { - // A small hack to facilitate links creation in QML. + // Two small hacks on originalJson to expose mediaIds to QML originalJson.insert("mediaId", InfoT::mediaId()); + originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); } QMimeType type() const override { return InfoT::mimeType; } const FileInfo* fileInfo() const override { return this; } FileInfo* fileInfo() override { return this; } - - protected: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - if (!InfoT::file.has_value()) { - json->insert("url", InfoT::url.toString()); - } else { - json->insert("file", Quotient::toJson(*InfoT::file)); - } - if (!InfoT::originalName.isEmpty()) - json->insert("filename", InfoT::originalName); - json->insert("info", toInfoJson(*this)); - } - }; - - template - class QUOTIENT_API UrlWithThumbnailContent : public UrlBasedContent { - public: - // NB: when using inherited constructors, thumbnail has to be - // initialised separately - using UrlBasedContent::UrlBasedContent; - explicit UrlWithThumbnailContent(const QJsonObject& json) - : UrlBasedContent(json), thumbnail(InfoT::originalInfoJson) - { - // Another small hack, to simplify making a thumbnail link - UrlBasedContent::originalJson.insert("thumbnailMediaId", - thumbnail.mediaId()); - } - const Thumbnail* thumbnailInfo() const override { return &thumbnail; } public: Thumbnail thumbnail; protected: - void fillJson(QJsonObject* json) const override + virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const + {} + + void fillJson(QJsonObject& json) const override { - UrlBasedContent::fillJson(json); - auto infoJson = json->take("info").toObject(); - thumbnail.fillInfoJson(&infoJson); - json->insert("info", infoJson); + if (!InfoT::file.has_value()) { + json.insert("url", InfoT::url.toString()); + } else { + json.insert("file", Quotient::toJson(*InfoT::file)); + } + if (!InfoT::originalName.isEmpty()) + json.insert("filename", InfoT::originalName); + auto infoJson = toInfoJson(*this); + if (thumbnail.isValid()) + thumbnail.dumpTo(infoJson); + fillInfoJson(infoJson); + json.insert("info", infoJson); } }; diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index b4770224..55da5809 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -43,19 +43,18 @@ MemberEventContent::MemberEventContent(const QJsonObject& json) displayName = sanitized(*displayName); } -void MemberEventContent::fillJson(QJsonObject* o) const +void MemberEventContent::fillJson(QJsonObject& o) const { - Q_ASSERT(o); if (membership != Membership::Invalid) - o->insert(QStringLiteral("membership"), + o.insert(QStringLiteral("membership"), MembershipStrings[qCountTrailingZeroBits( std::underlying_type_t(membership))]); if (displayName) - o->insert(QStringLiteral("displayname"), *displayName); + o.insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) - o->insert(QStringLiteral("avatar_url"), avatarUrl->toString()); + o.insert(QStringLiteral("avatar_url"), avatarUrl->toString()); if (!reason.isEmpty()) - o->insert(QStringLiteral("reason"), reason); + o.insert(QStringLiteral("reason"), reason); } bool RoomMemberEvent::changesMembership() const diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index ceb7826b..afbaf825 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -26,7 +26,7 @@ public: QString reason; protected: - void fillJson(QJsonObject* o) const override; + void fillJson(QJsonObject& o) const override; }; using MembershipType [[deprecated("Use Membership instead")]] = Membership; diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index d63352cb..d9d3fbe0 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -302,17 +302,16 @@ TextContent::TextContent(const QJsonObject& json) } } -void TextContent::fillJson(QJsonObject* json) const +void TextContent::fillJson(QJsonObject &json) const { static const auto FormatKey = QStringLiteral("format"); - Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(FormatKey, HtmlContentTypeId); - json->insert(FormattedBodyKey, body); + json.insert(FormatKey, HtmlContentTypeId); + json.insert(FormattedBodyKey, body); } if (relatesTo) { - json->insert( + json.insert( QStringLiteral("m.relates_to"), relatesTo->type == EventRelation::ReplyType ? QJsonObject { { relatesTo->type, @@ -326,7 +325,7 @@ void TextContent::fillJson(QJsonObject* json) const newContentJson.insert(FormatKey, HtmlContentTypeId); newContentJson.insert(FormattedBodyKey, body); } - json->insert(QStringLiteral("m.new_content"), newContentJson); + json.insert(QStringLiteral("m.new_content"), newContentJson); } } } @@ -347,9 +346,8 @@ QMimeType LocationContent::type() const return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); } -void LocationContent::fillJson(QJsonObject* o) const +void LocationContent::fillJson(QJsonObject& o) const { - Q_ASSERT(o); - o->insert(QStringLiteral("geo_uri"), geoUri); - o->insert(QStringLiteral("info"), toInfoJson(thumbnail)); + o.insert(QStringLiteral("geo_uri"), geoUri); + o.insert(QStringLiteral("info"), toInfoJson(thumbnail)); } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 03a51328..6968ad70 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -136,7 +136,7 @@ namespace EventContent { Omittable relatesTo; protected: - void fillJson(QJsonObject* json) const override; + void fillJson(QJsonObject& json) const override; }; /** @@ -164,28 +164,25 @@ namespace EventContent { Thumbnail thumbnail; protected: - void fillJson(QJsonObject* o) const override; + void fillJson(QJsonObject& o) const override; }; /** * A base class for info types that include duration: audio and video */ - template - class QUOTIENT_API PlayableContent : public ContentT { + template + class PlayableContent : public UrlBasedContent { public: - using ContentT::ContentT; + using UrlBasedContent::UrlBasedContent; PlayableContent(const QJsonObject& json) - : ContentT(json) - , duration(ContentT::originalInfoJson["duration"_ls].toInt()) + : UrlBasedContent(json) + , duration(FileInfo::originalInfoJson["duration"_ls].toInt()) {} protected: - void fillJson(QJsonObject* json) const override + void fillInfoJson(QJsonObject& infoJson) const override { - ContentT::fillJson(json); - auto infoJson = json->take("info"_ls).toObject(); infoJson.insert(QStringLiteral("duration"), duration); - json->insert(QStringLiteral("info"), infoJson); } public: @@ -211,7 +208,7 @@ namespace EventContent { * - mimeType * - imageSize */ - using VideoContent = PlayableContent>; + using VideoContent = PlayableContent; /** * Content class for m.audio @@ -224,7 +221,13 @@ namespace EventContent { * - payloadSize ("size" in JSON) * - mimeType ("mimetype" in JSON) * - duration + * - thumbnail.url ("thumbnail_url" in JSON - extension to the spec) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field (extension to the spec): + * - payloadSize + * - mimeType + * - imageSize */ - using AudioContent = PlayableContent>; + using AudioContent = PlayableContent; } // namespace EventContent } // namespace Quotient diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp index 8d262ddf..1c87fd47 100644 --- a/lib/events/roompowerlevelsevent.cpp +++ b/lib/events/roompowerlevelsevent.cpp @@ -21,17 +21,17 @@ PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : { } -void PowerLevelsEventContent::fillJson(QJsonObject* o) const { - o->insert(QStringLiteral("invite"), invite); - o->insert(QStringLiteral("kick"), kick); - o->insert(QStringLiteral("ban"), ban); - o->insert(QStringLiteral("redact"), redact); - o->insert(QStringLiteral("events"), Quotient::toJson(events)); - o->insert(QStringLiteral("events_default"), eventsDefault); - o->insert(QStringLiteral("state_default"), stateDefault); - o->insert(QStringLiteral("users"), Quotient::toJson(users)); - o->insert(QStringLiteral("users_default"), usersDefault); - o->insert(QStringLiteral("notifications"), QJsonObject{{"room", notifications.room}}); +void PowerLevelsEventContent::fillJson(QJsonObject& o) const { + o.insert(QStringLiteral("invite"), invite); + o.insert(QStringLiteral("kick"), kick); + o.insert(QStringLiteral("ban"), ban); + o.insert(QStringLiteral("redact"), redact); + o.insert(QStringLiteral("events"), Quotient::toJson(events)); + o.insert(QStringLiteral("events_default"), eventsDefault); + o.insert(QStringLiteral("state_default"), stateDefault); + o.insert(QStringLiteral("users"), Quotient::toJson(users)); + o.insert(QStringLiteral("users_default"), usersDefault); + o.insert(QStringLiteral("notifications"), QJsonObject{{"room", notifications.room}}); } int RoomPowerLevelsEvent::powerLevelForEvent(const QString &eventId) const { diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index 415cc814..3ce83763 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -31,7 +31,7 @@ public: Notifications notifications; protected: - void fillJson(QJsonObject* o) const override; + void fillJson(QJsonObject& o) const override; }; class QUOTIENT_API RoomPowerLevelsEvent -- cgit v1.2.3 From 4070706fcc91cd46054b00c5f3a267a9d8c44fb7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 5 Jan 2022 13:58:47 +0100 Subject: Event content: provide toJson() instead of deriving from EC::Base EventContent::Base has been made primarily for the sake of dynamic polymorphism needed within RoomMessageEvent content (arguably, it might not be really needed even there, but that's a bigger matter for another time). When that polymorphism is not needed, it's easier for reading and maintenance to have toJson() member function (or even specialise JsonConverter<> outside of the content structure) instead of deriving from EC::Base and then still having fillJson() member function. This commit removes EventContent::Base dependency where it's not beneficial. --- lib/events/encryptionevent.cpp | 4 +++- lib/events/encryptionevent.h | 8 +++----- lib/events/roommemberevent.cpp | 6 +++--- lib/events/roommemberevent.h | 7 ++----- lib/events/roompowerlevelsevent.cpp | 10 ++++++---- lib/events/roompowerlevelsevent.h | 8 ++------ 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 47b0a032..6e994cd4 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -47,10 +47,12 @@ EncryptionEventContent::EncryptionEventContent(EncryptionType et) } } -void EncryptionEventContent::fillJson(QJsonObject& o) const +QJsonObject EncryptionEventContent::toJson() const { + QJsonObject o; if (encryption != EncryptionType::Undefined) o.insert(AlgorithmKey, algorithm); o.insert(RotationPeriodMsKey, rotationPeriodMs); o.insert(RotationPeriodMsgsKey, rotationPeriodMsgs); + return o; } diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index fe76b1af..5b5420ec 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -4,12 +4,11 @@ #pragma once -#include "eventcontent.h" #include "stateevent.h" #include "quotient_common.h" namespace Quotient { -class QUOTIENT_API EncryptionEventContent : public EventContent::Base { +class QUOTIENT_API EncryptionEventContent { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; @@ -20,13 +19,12 @@ public: {} explicit EncryptionEventContent(const QJsonObject& json); + QJsonObject toJson() const; + EncryptionType encryption; QString algorithm; int rotationPeriodMs; int rotationPeriodMsgs; - -protected: - void fillJson(QJsonObject& o) const override; }; using EncryptionType = EncryptionEventContent::EncryptionType; diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 55da5809..c3be0e00 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -4,8 +4,6 @@ #include "roommemberevent.h" -#include "logging.h" - #include namespace Quotient { @@ -43,8 +41,9 @@ MemberEventContent::MemberEventContent(const QJsonObject& json) displayName = sanitized(*displayName); } -void MemberEventContent::fillJson(QJsonObject& o) const +QJsonObject MemberEventContent::toJson() const { + QJsonObject o; if (membership != Membership::Invalid) o.insert(QStringLiteral("membership"), MembershipStrings[qCountTrailingZeroBits( @@ -55,6 +54,7 @@ void MemberEventContent::fillJson(QJsonObject& o) const o.insert(QStringLiteral("avatar_url"), avatarUrl->toString()); if (!reason.isEmpty()) o.insert(QStringLiteral("reason"), reason); + return o; } bool RoomMemberEvent::changesMembership() const diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index afbaf825..dd33ea6b 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -5,18 +5,18 @@ #pragma once -#include "eventcontent.h" #include "stateevent.h" #include "quotient_common.h" namespace Quotient { -class QUOTIENT_API MemberEventContent : public EventContent::Base { +class QUOTIENT_API MemberEventContent { public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {} explicit MemberEventContent(const QJsonObject& json); + QJsonObject toJson() const; Membership membership; /// (Only for invites) Whether the invite is to a direct chat @@ -24,9 +24,6 @@ public: Omittable displayName; Omittable avatarUrl; QString reason; - -protected: - void fillJson(QJsonObject& o) const override; }; using MembershipType [[deprecated("Use Membership instead")]] = Membership; diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp index 1c87fd47..84a31d55 100644 --- a/lib/events/roompowerlevelsevent.cpp +++ b/lib/events/roompowerlevelsevent.cpp @@ -3,8 +3,6 @@ #include "roompowerlevelsevent.h" -#include - using namespace Quotient; PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : @@ -21,7 +19,9 @@ PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : { } -void PowerLevelsEventContent::fillJson(QJsonObject& o) const { +QJsonObject PowerLevelsEventContent::toJson() const +{ + QJsonObject o; o.insert(QStringLiteral("invite"), invite); o.insert(QStringLiteral("kick"), kick); o.insert(QStringLiteral("ban"), ban); @@ -31,7 +31,9 @@ void PowerLevelsEventContent::fillJson(QJsonObject& o) const { o.insert(QStringLiteral("state_default"), stateDefault); o.insert(QStringLiteral("users"), Quotient::toJson(users)); o.insert(QStringLiteral("users_default"), usersDefault); - o.insert(QStringLiteral("notifications"), QJsonObject{{"room", notifications.room}}); + o.insert(QStringLiteral("notifications"), + QJsonObject { { "room", notifications.room } }); + return o; } int RoomPowerLevelsEvent::powerLevelForEvent(const QString &eventId) const { diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index 3ce83763..a1638a27 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -3,17 +3,16 @@ #pragma once -#include "eventcontent.h" #include "stateevent.h" namespace Quotient { -class QUOTIENT_API PowerLevelsEventContent : public EventContent::Base { -public: +struct QUOTIENT_API PowerLevelsEventContent { struct Notifications { int room; }; explicit PowerLevelsEventContent(const QJsonObject& json); + QJsonObject toJson() const; int invite; int kick; @@ -29,9 +28,6 @@ public: int usersDefault; Notifications notifications; - -protected: - void fillJson(QJsonObject& o) const override; }; class QUOTIENT_API RoomPowerLevelsEvent -- cgit v1.2.3 From 843f7a0ac2619a2f2863d457cdeaa03707255793 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 4 May 2022 21:12:29 +0200 Subject: basic*EventJson() -> *Event::basicJson() This makes it easier and more intuitive to build a minimal JSON payload for a given event type. A common basicJson() call point is also convenient in template contexts (see next commits). --- lib/connection.cpp | 4 ++-- lib/events/event.cpp | 2 +- lib/events/event.h | 14 +++++++------- lib/events/eventloader.h | 4 ++-- lib/events/roomevent.cpp | 10 +++++----- lib/events/roomevent.h | 5 +++++ lib/events/stateevent.cpp | 2 +- lib/events/stateevent.h | 28 ++++++++++++++++++---------- quotest/quotest.cpp | 8 ++++---- 9 files changed, 45 insertions(+), 32 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a969b3b9..1ea394a1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1858,11 +1858,11 @@ void Connection::saveState() const } { QJsonArray accountDataEvents { - basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats)) + Event::basicJson(QStringLiteral("m.direct"), toJson(d->directChats)) }; for (const auto& e : d->accountData) accountDataEvents.append( - basicEventJson(e.first, e.second->contentJson())); + Event::basicJson(e.first, e.second->contentJson())); rootObj.insert(QStringLiteral("account_data"), QJsonObject { diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 4c304a3c..1f1eebaa 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -29,7 +29,7 @@ Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) } Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) - : Event(type, basicEventJson(matrixType, contentJson)) + : Event(type, basicJson(matrixType, contentJson)) {} Event::~Event() = default; diff --git a/lib/events/event.h b/lib/events/event.h index 113fa3fa..5be2b41b 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -48,13 +48,6 @@ const QString RoomIdKey { RoomIdKeyL }; const QString UnsignedKey { UnsignedKeyL }; const QString StateKeyKey { StateKeyKeyL }; -/// Make a minimal correct Matrix event JSON -inline QJsonObject basicEventJson(const QString& matrixType, - const QJsonObject& content) -{ - return { { TypeKey, matrixType }, { ContentKey, content } }; -} - // === Event types === using event_type_t = QLatin1String; @@ -193,6 +186,13 @@ public: Event& operator=(Event&&) = delete; virtual ~Event(); + /// Make a minimal correct Matrix event JSON + static QJsonObject basicJson(const QString& matrixType, + const QJsonObject& content) + { + return { { TypeKey, matrixType }, { ContentKey, content } }; + } + Type type() const { return _type; } QString matrixType() const; [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() " diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index fe624d70..c7b82e8e 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -29,7 +29,7 @@ template inline event_ptr_tt loadEvent(const QString& matrixType, const QJsonObject& content) { - return doLoadEvent(basicEventJson(matrixType, content), + return doLoadEvent(Event::basicJson(matrixType, content), matrixType); } @@ -44,7 +44,7 @@ inline StateEventPtr loadStateEvent(const QString& matrixType, const QString& stateKey = {}) { return doLoadEvent( - basicStateEventJson(matrixType, content, stateKey), matrixType); + StateEventBase::basicJson(matrixType, content, stateKey), matrixType); } template diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 2f482871..ebe72149 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -101,19 +101,19 @@ void RoomEvent::dumpTo(QDebug dbg) const dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; } -QJsonObject makeCallContentJson(const QString& callId, int version, - QJsonObject content) +QJsonObject CallEventBase::basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject content) { content.insert(QStringLiteral("call_id"), callId); content.insert(QStringLiteral("version"), version); - return content; + return RoomEvent::basicJson(matrixType, content); } CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson) - : RoomEvent(type, matrixType, - makeCallContentJson(callId, version, contentJson)) + : RoomEvent(type, basicJson(type, callId, version, contentJson)) {} CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index a7d6c428..0692ad8a 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -98,6 +98,11 @@ public: QString callId() const { return contentPart("call_id"_ls); } int version() const { return contentPart("version"_ls); } + +protected: + static QJsonObject basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson = {}); }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index e53d47d4..cdf3c449 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -16,7 +16,7 @@ StateEventBase::StateEventBase(Type type, const QJsonObject& json) StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) - : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) + : RoomEvent(type, basicJson(type, contentJson, stateKey)) {} bool StateEventBase::repeatsState() const diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 19905431..47bf6e59 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -7,16 +7,6 @@ namespace Quotient { -/// Make a minimal correct Matrix state event JSON -inline QJsonObject basicStateEventJson(const QString& matrixTypeId, - const QJsonObject& content, - const QString& stateKey = {}) -{ - return { { TypeKey, matrixTypeId }, - { StateKeyKey, stateKey }, - { ContentKey, content } }; -} - class QUOTIENT_API StateEventBase : public RoomEvent { public: static inline EventFactory factory { "StateEvent" }; @@ -27,6 +17,16 @@ public: const QJsonObject& contentJson = {}); ~StateEventBase() override = default; + //! Make a minimal correct Matrix state event JSON + static QJsonObject basicJson(const QString& matrixTypeId, + const QJsonObject& content, + const QString& stateKey = {}) + { + return { { TypeKey, matrixTypeId }, + { StateKeyKey, stateKey }, + { ContentKey, content } }; + } + bool isStateEvent() const override { return true; } QString replacedState() const; void dumpTo(QDebug dbg) const override; @@ -36,6 +36,14 @@ public: using StateEventPtr = event_ptr_tt; using StateEvents = EventsArray; +[[deprecated("Use StateEventBase::basicJson() instead")]] +inline QJsonObject basicStateEventJson(const QString& matrixTypeId, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return StateEventBase::basicJson(matrixTypeId, content, stateKey); +} + //! \brief Override RoomEvent factory with that from StateEventBase if JSON has //! stateKey //! diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 792faabd..861cfba0 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -531,10 +531,10 @@ public: : RoomEvent(typeId(), jo) {} CustomEvent(int testValue) - : RoomEvent(typeId(), - basicEventJson(matrixTypeId(), - QJsonObject { { "testValue"_ls, - toJson(testValue) } })) + : RoomEvent(TypeId, + Event::basicJson(TypeId, + QJsonObject { { "testValue"_ls, + toJson(testValue) } })) {} auto testValue() const { return contentPart("testValue"_ls); } -- cgit v1.2.3 From 41f630cf7be6e806e498cb31711f403bf6919ff8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 7 May 2022 19:23:18 +0200 Subject: Pass matrixType(QString), not Event::Type, to basicJson() Not that it was very important, as the two are basically the same thing (and matrixTypeId() is about to be obsoleted); but formally basicJson() is supposed to get the former, not the latter. --- lib/events/roomevent.cpp | 8 ++++---- lib/events/stateevent.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index ebe72149..707cf4fd 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -105,15 +105,15 @@ QJsonObject CallEventBase::basicJson(const QString& matrixType, const QString& callId, int version, QJsonObject content) { - content.insert(QStringLiteral("call_id"), callId); - content.insert(QStringLiteral("version"), version); - return RoomEvent::basicJson(matrixType, content); + contentJson.insert(QStringLiteral("call_id"), callId); + contentJson.insert(QStringLiteral("version"), version); + return RoomEvent::basicJson(matrixType, contentJson); } CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson) - : RoomEvent(type, basicJson(type, callId, version, contentJson)) + : RoomEvent(type, basicJson(matrixType, callId, version, contentJson)) {} CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index cdf3c449..0fd489d1 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -16,7 +16,7 @@ StateEventBase::StateEventBase(Type type, const QJsonObject& json) StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) - : RoomEvent(type, basicJson(type, contentJson, stateKey)) + : RoomEvent(type, basicJson(matrixType, contentJson, stateKey)) {} bool StateEventBase::repeatsState() const -- cgit v1.2.3 From e7a4b5a545b0f59b95ca8097009dbf6eea534db1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 6 May 2022 22:17:55 +0200 Subject: Generalise DEFINE_SIMPLE_EVENT This macro was defined in accountdataevents.h but adding one more parameter (base class) makes it applicable to pretty much any event with the content that has one key-value pair (though state events already have a non-macro solution in the form of `StateEvent`). Now CustomEvent definition in quotest.cpp can be replaced with a single line. --- lib/events/accountdataevents.h | 31 +++++++++---------------------- lib/events/event.h | 26 ++++++++++++++++++++++++++ quotest/quotest.cpp | 18 +----------------- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 12f1f00b..ec2f64e3 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -46,27 +46,14 @@ struct JsonObjectConverter { using TagsMap = QHash; -#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class QUOTIENT_API _Name : public Event { \ - public: \ - using content_type = _ContentType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \ - explicit _Name(const content_type& content) \ - : Event(typeId(), matrixTypeId(), \ - QJsonObject { \ - { QStringLiteral(#_ContentKey), toJson(content) } }) \ - {} \ - auto _ContentKey() const \ - { \ - return contentPart(#_ContentKey##_ls); \ - } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ - // End of macro - -DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags) -DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id) -DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", QSet, +DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags) +DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, eventId) +class ReadMarkerEvent : public ReadMarkerEventImpl { +public: + using ReadMarkerEventImpl::ReadMarkerEventImpl; + [[deprecated("Use ReadMarkerEvent::eventId() instead")]] + QString event_id() const { return eventId(); } +}; +DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, Event, "m.ignored_user_list", QSet, ignored_users) } // namespace Quotient diff --git a/lib/events/event.h b/lib/events/event.h index 5be2b41b..a27c0b96 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -278,6 +278,32 @@ using Events = EventsArray; Type_::factory.addMethod(); \ // End of macro +/// \brief Define a new event class with a single key-value pair in the content +/// +/// This macro defines a new event class \p Name_ derived from \p Base_, +/// with Matrix event type \p TypeId_, providing a getter named \p GetterName_ +/// for a single value of type \p ValueType_ inside the event content. +/// To retrieve the value the getter uses a JSON key name that corresponds to +/// its own (getter's) name but written in snake_case. \p GetterName_ must be +/// in camelCase, no quotes (an identifier, not a literal). +#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_) \ + class QUOTIENT_API Name_ : public Base_ { \ + public: \ + using content_type = ValueType_; \ + DEFINE_EVENT_TYPEID(TypeId_, Name_) \ + explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \ + explicit Name_(const content_type& content) \ + : Name_(Base_::basicJson(TypeId, { { JsonKey, toJson(content) } })) \ + {} \ + auto GetterName_() const \ + { \ + return contentPart(JsonKey); \ + } \ + static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \ + }; \ + REGISTER_EVENT_TYPE(Name_) \ + // End of macro + // === is<>(), eventCast<>() and switchOnType<>() === template diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 861cfba0..b14442b9 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -523,23 +523,7 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return true; } -class CustomEvent : public RoomEvent { -public: - DEFINE_EVENT_TYPEID("quotest.custom", CustomEvent) - - CustomEvent(const QJsonObject& jo) - : RoomEvent(typeId(), jo) - {} - CustomEvent(int testValue) - : RoomEvent(TypeId, - Event::basicJson(TypeId, - QJsonObject { { "testValue"_ls, - toJson(testValue) } })) - {} - - auto testValue() const { return contentPart("testValue"_ls); } -}; -REGISTER_EVENT_TYPE(CustomEvent) +DEFINE_SIMPLE_EVENT(CustomEvent, RoomEvent, "quotest.custom", int, testValue) TEST_IMPL(sendCustomEvent) { -- 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/events/directchatevent.cpp | 2 -- lib/events/eventcontent.h | 2 +- lib/events/roomevent.cpp | 4 ++-- lib/events/stateevent.cpp | 4 ++-- lib/room.cpp | 9 +++++---- lib/room.h | 3 ++- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index 0ee1f7b0..83bb1e32 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -3,8 +3,6 @@ #include "directchatevent.h" -#include - using namespace Quotient; QMultiHash DirectChatEvent::usersToDirectChats() const diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 6aee333d..bbd35618 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -40,7 +40,7 @@ namespace EventContent { Base(const Base&) = default; Base(Base&&) = default; - virtual void fillJson(QJsonObject& o) const = 0; + virtual void fillJson(QJsonObject&) const = 0; }; // The below structures fairly follow CS spec 11.2.1.6. The overall diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 707cf4fd..3ddf5ac4 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -103,7 +103,7 @@ void RoomEvent::dumpTo(QDebug dbg) const QJsonObject CallEventBase::basicJson(const QString& matrixType, const QString& callId, int version, - QJsonObject content) + QJsonObject contentJson) { contentJson.insert(QStringLiteral("call_id"), callId); contentJson.insert(QStringLiteral("version"), version); @@ -116,7 +116,7 @@ CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, : RoomEvent(type, basicJson(matrixType, callId, version, contentJson)) {} -CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) +CallEventBase::CallEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) { if (callId().isEmpty()) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 0fd489d1..c343e37f 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -6,9 +6,9 @@ using namespace Quotient; StateEventBase::StateEventBase(Type type, const QJsonObject& json) - : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json) + : RoomEvent(json.contains(StateKeyKeyL) ? type : UnknownEventTypeId, json) { - if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL)) + if (Event::type() == UnknownEventTypeId && !json.contains(StateKeyKeyL)) qWarning(EVENTS) << "Attempt to create a state event with no stateKey -" "forcing the event type to unknown to avoid damage"; } 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(); } diff --git a/lib/room.h b/lib/room.h index 6ba7feac..7e53aed0 100644 --- a/lib/room.h +++ b/lib/room.h @@ -758,7 +758,8 @@ public: [[deprecated("Use currentState().get() instead; " "make sure to check its result for nullptrs")]] // const Quotient::StateEventBase* - getCurrentState(const QString& evtType, const QString& stateKey = {}) const; + getCurrentState(const QString& evtType, + const QString& stateKey = {}) const; /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of -- cgit v1.2.3 From 4b35dfd8af196ff9e8669499ea3ed7e4127f5901 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 30 Jan 2022 15:55:06 +0100 Subject: Use std::pair instead of QPair QPair is giving way to its STL counterpart, becoming its alias in Qt 6. --- lib/events/stateevent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 47bf6e59..343e87a5 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -72,7 +72,7 @@ inline bool is(const Event& e) * \sa * https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events */ -using StateEventKey = QPair; +using StateEventKey = std::pair; template struct Prev { -- cgit v1.2.3 From c42d268db0b40cdba06381fc64a6966a72c90709 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 11 Feb 2022 22:21:25 +0100 Subject: QUO_CONTENT_GETTER To streamline adding of simple getters of content parts. --- lib/events/callanswerevent.h | 6 ++---- lib/events/callcandidatesevent.h | 17 +++-------------- lib/events/callinviteevent.h | 5 +---- lib/events/event.h | 15 +++++++++++++++ lib/events/redactionevent.h | 2 +- lib/events/roomevent.h | 4 ++-- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 8ffe60f2..70292a7a 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -17,10 +17,8 @@ public: const QString& sdp); explicit CallAnswerEvent(const QString& callId, const QString& sdp); - int lifetime() const - { - return contentPart("lifetime"_ls); - } // FIXME: Omittable<>? + QUO_CONTENT_GETTER(int, lifetime) // FIXME: Omittable<>? + QString sdp() const { return contentPart("answer"_ls).value("sdp"_ls).toString(); diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index 74c38f2c..e949f722 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -23,20 +23,9 @@ public: { { QStringLiteral("candidates"), candidates } }) {} - QJsonArray candidates() const - { - return contentPart("candidates"_ls); - } - - QString callId() const - { - return contentPart("call_id"); - } - - int version() const - { - return contentPart("version"); - } + QUO_CONTENT_GETTER(QJsonArray, candidates) + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) }; REGISTER_EVENT_TYPE(CallCandidatesEvent) diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 47362b5c..1b1f4f0f 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -16,10 +16,7 @@ public: explicit CallInviteEvent(const QString& callId, const int lifetime, const QString& sdp); - int lifetime() const - { - return contentPart("lifetime"_ls); - } // FIXME: Omittable<>? + QUO_CONTENT_GETTER(int, lifetime) // FIXME: Omittable<>? QString sdp() const { return contentPart("offer"_ls).value("sdp"_ls).toString(); diff --git a/lib/events/event.h b/lib/events/event.h index a27c0b96..ec21c6aa 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -258,6 +258,21 @@ template using EventsArray = std::vector>; using Events = EventsArray; +//! \brief Define an inline method obtaining a content part +//! +//! This macro adds a const method that extracts a JSON value at the key +//! toSnakeCase(PartName_) (sic) and converts it to the type +//! \p PartType_. Effectively, the generated method is an equivalent of +//! \code +//! contentPart(Quotient::toSnakeCase(#PartName_##_ls)); +//! \endcode +#define QUO_CONTENT_GETTER(PartType_, PartName_) \ + PartType_ PartName_() const \ + { \ + static const auto JsonKey = toSnakeCase(#PartName_##_ls); \ + return contentPart(JsonKey); \ + } + // === Facilities for event class definitions === // This macro should be used in a public section of an event class to diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index be20bf52..1c486a44 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -17,7 +17,7 @@ public: { return fullJson()["redacts"_ls].toString(); } - QString reason() const { return contentPart("reason"_ls); } + QUO_CONTENT_GETTER(QString, reason) }; REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 0692ad8a..7f724689 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -96,8 +96,8 @@ public: ~CallEventBase() override = default; bool isCallEvent() const override { return true; } - QString callId() const { return contentPart("call_id"_ls); } - int version() const { return contentPart("version"_ls); } + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) protected: static QJsonObject basicJson(const QString& matrixType, -- 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/connection.cpp | 24 +++++++++++++----------- lib/jobs/syncjob.h | 2 +- lib/room.cpp | 11 ++++++----- lib/syncdata.cpp | 10 +++++----- lib/syncdata.h | 10 +++++----- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 1ea394a1..4418958e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -96,7 +96,6 @@ public: /// as of the last sync QHash roomAliasMap; QVector roomIdsToForget; - QVector firstTimeRooms; QVector pendingStateRoomIds; QMap userMap; DirectChatsMap directChats; @@ -833,16 +832,14 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, } if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) { pendingStateRoomIds.removeOne(roomData.roomId); - r->updateData(std::move(roomData), fromCache); - if (firstTimeRooms.removeOne(r)) { - emit q->loadedRoomState(r); - if (capabilities.roomVersions) - r->checkVersion(); - // Otherwise, the version will be checked in reloadCapabilities() - } + // Update rooms one by one, giving time to update the UI. + QMetaObject::invokeMethod( + r, + [r, rd = std::move(roomData), fromCache] () mutable { + r->updateData(std::move(rd), fromCache); + }, + Qt::QueuedConnection); } - // Let UI update itself after updating each room - QCoreApplication::processEvents(); } } @@ -1707,9 +1704,14 @@ Room* Connection::provideRoom(const QString& id, Omittable joinState) return nullptr; } d->roomMap.insert(roomKey, room); - d->firstTimeRooms.push_back(room); connect(room, &Room::beforeDestruction, this, &Connection::aboutToDeleteRoom); + connect(room, &Room::baseStateLoaded, this, [this, room] { + emit loadedRoomState(room); + if (d->capabilities.roomVersions) + room->checkVersion(); + // Otherwise, the version will be checked in reloadCapabilities() + }); emit newRoom(room); } if (!joinState) diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 830a7c71..b7bfbbb3 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -15,7 +15,7 @@ public: explicit SyncJob(const QString& since, const Filter& filter, int timeout = -1, const QString& presence = {}); - SyncData&& takeData() { return std::move(d); } + SyncData takeData() { return std::move(d); } protected: Status prepareResult() override; 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) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 78957cbe..95d3c7e4 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -142,7 +142,7 @@ SyncData::SyncData(const QString& cacheFileName) << "is required; discarding the cache"; } -SyncDataList&& SyncData::takeRoomData() { return move(roomData); } +SyncDataList SyncData::takeRoomData() { return move(roomData); } QString SyncData::fileNameForRoom(QString roomId) { @@ -150,18 +150,18 @@ QString SyncData::fileNameForRoom(QString roomId) return roomId + ".json"; } -Events&& SyncData::takePresenceData() { return std::move(presenceData); } +Events SyncData::takePresenceData() { return std::move(presenceData); } -Events&& SyncData::takeAccountData() { return std::move(accountData); } +Events SyncData::takeAccountData() { return std::move(accountData); } -Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } +Events SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } std::pair SyncData::cacheVersion() { return { MajorCacheVersion, 2 }; } -DevicesList&& SyncData::takeDevicesList() { return std::move(devicesList); } +DevicesList SyncData::takeDevicesList() { return std::move(devicesList); } QJsonObject SyncData::loadJson(const QString& fileName) { diff --git a/lib/syncdata.h b/lib/syncdata.h index 6b70140d..9358ec8f 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -98,15 +98,15 @@ public: */ void parseJson(const QJsonObject& json, const QString& baseDir = {}); - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); + Events takePresenceData(); + Events takeAccountData(); + Events takeToDeviceEvents(); const QHash& deviceOneTimeKeysCount() const { return deviceOneTimeKeysCount_; } - SyncDataList&& takeRoomData(); - DevicesList&& takeDevicesList(); + SyncDataList takeRoomData(); + DevicesList takeDevicesList(); QString nextBatch() const { return nextBatch_; } -- 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/events/callanswerevent.cpp | 11 ----------- lib/events/callanswerevent.h | 4 ---- lib/room.cpp | 7 ++++--- lib/room.h | 5 +++-- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index be83d9d0..f75f8ad3 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -14,7 +14,6 @@ m.call.answer "type": "answer" }, "call_id": "12345", - "lifetime": 60000, "version": 0 }, "event_id": "$WLGTSEFSEF:localhost", @@ -33,16 +32,6 @@ CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) qCDebug(EVENTS) << "Call Answer event"; } -CallAnswerEvent::CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp) - : CallEventBase( - typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("lifetime"), lifetime }, - { QStringLiteral("answer"), - QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } }) -{} - CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) : CallEventBase( typeId(), matrixTypeId(), callId, 0, diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 70292a7a..4d539b85 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -13,12 +13,8 @@ public: explicit CallAnswerEvent(const QJsonObject& obj); - explicit CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp); explicit CallAnswerEvent(const QString& callId, const QString& sdp); - QUO_CONTENT_GETTER(int, lifetime) // FIXME: Omittable<>? - QString sdp() const { return contentPart("answer"_ls).value("sdp"_ls).toString(); 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) diff --git a/lib/room.h b/lib/room.h index 7e53aed0..2e2ebf9a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -871,8 +871,9 @@ public Q_SLOTS: void inviteCall(const QString& callId, const int lifetime, const QString& sdp); void sendCallCandidates(const QString& callId, const QJsonArray& candidates); - void answerCall(const QString& callId, const int lifetime, - const QString& sdp); + [[deprecated("Lifetime argument is no more in use here; " + "use 2-arg Room::answerCall() instead")]] + void answerCall(const QString& callId, int lifetime, const QString& sdp); void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); -- 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/connection.cpp | 72 ++++++++++++++++++++--------------------- lib/connection.h | 6 ++-- lib/converters.h | 8 +++++ lib/e2ee/qolminboundsession.cpp | 4 +-- lib/e2ee/qolminboundsession.h | 2 +- lib/events/callinviteevent.cpp | 2 +- lib/events/callinviteevent.h | 4 +-- lib/events/roomkeyevent.h | 5 ++- lib/room.cpp | 20 ++++++------ lib/room.h | 4 +-- 10 files changed, 70 insertions(+), 57 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4418958e..bd4d9251 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -219,7 +219,9 @@ public: } q->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult), QDateTime::currentDateTime()); } - std::pair sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr& olmAccount) + + std::pair sessionDecryptPrekey(const QOlmMessage& message, + const QString& senderKey) { Q_ASSERT(message.type() == QOlmMessage::PreKey); for(auto& session : olmSessions[senderKey]) { @@ -273,24 +275,19 @@ public: } std::pair sessionDecryptMessage( - const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr& account) + const QJsonObject& personalCipherObject, const QByteArray& senderKey) { - QString decrypted; - QString olmSessionId; - int type = personalCipherObject.value(TypeKeyL).toInt(-1); - QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); - if (type == QOlmMessage::PreKey) { - QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); - auto result = sessionDecryptPrekey(preKeyMessage, senderKey, account); - decrypted = result.first; - olmSessionId = result.second; - } else if (type == QOlmMessage::General) { - QOlmMessage message(body, QOlmMessage::General); - auto result = sessionDecryptGeneral(message, senderKey); - decrypted = result.first; - olmSessionId = result.second; - } - return { decrypted, olmSessionId }; + QOlmMessage message { + personalCipherObject.value(BodyKeyL).toString().toLatin1(), + static_cast( + personalCipherObject.value(TypeKeyL).toInt(-1)) + }; + if (message.type() == QOlmMessage::PreKey) + return sessionDecryptPrekey(message, senderKey); + if (message.type() == QOlmMessage::General) + return sessionDecryptGeneral(message, senderKey); + qCWarning(E2EE) << "Olm message has incorrect type" << message.type(); + return {}; } #endif @@ -310,8 +307,9 @@ public: qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } - const auto [decrypted, olmSessionId] = sessionDecryptMessage( - personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount); + const auto [decrypted, olmSessionId] = + sessionDecryptMessage(personalCipherObject, + encryptedEvent.senderKey().toLatin1()); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" << encryptedEvent.senderKey() @@ -950,8 +948,6 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) outdatedUsers += event.senderId(); encryptionUpdateRequired = true; pendingEncryptedEvents.push_back(std::make_unique(event.fullJson())); - }, [](const Event& e){ - // Unhandled }); } #endif @@ -967,7 +963,7 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve } switchOnType(*decryptedEvent, - [this, senderKey = event.senderKey(), &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { + [this, &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); } else { @@ -2074,12 +2070,13 @@ void Connection::Private::loadOutdatedUserDevices() saveDevicesList(); for(size_t i = 0; i < pendingEncryptedEvents.size();) { - if (q->isKnownCurveKey(pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), pendingEncryptedEvents[i]->contentJson()["sender_key"].toString())) { - handleEncryptedToDeviceEvent(*(pendingEncryptedEvents[i].get())); + if (q->isKnownCurveKey( + pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), + pendingEncryptedEvents[i]->contentPart("sender_key"_ls))) { + handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]); pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i); - } else { - i++; - } + } else + ++i; } }); } @@ -2188,13 +2185,10 @@ void Connection::saveOlmAccount() #ifdef Quotient_E2EE_ENABLED QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) { - auto room = this->room(notification["room_id"].toString()); + auto r = room(notification["room_id"].toString()); auto event = makeEvent(notification["event"].toObject()); - auto decrypted = room->decryptMessage(*event); - if(!decrypted) { - return QJsonObject(); - } - return decrypted->fullJson(); + const auto decrypted = r->decryptMessage(*event); + return decrypted ? decrypted->fullJson() : QJsonObject(); } Database* Connection::database() @@ -2202,14 +2196,18 @@ Database* Connection::database() return d->database; } -UnorderedMap Connection::loadRoomMegolmSessions(Room* room) +UnorderedMap +Connection::loadRoomMegolmSessions(const Room* room) { return database()->loadMegolmSessions(room->id(), picklingMode()); } -void Connection::saveMegolmSession(Room* room, QOlmInboundGroupSession* session) +void Connection::saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session) { - database()->saveMegolmSession(room->id(), session->sessionId(), session->pickle(picklingMode()), session->senderId(), session->olmSessionId()); + database()->saveMegolmSession(room->id(), session.sessionId(), + session.pickle(picklingMode()), + session.senderId(), session.olmSessionId()); } QStringList Connection::devicesForUser(User* user) const diff --git a/lib/connection.h b/lib/connection.h index 29731593..b75bd5b5 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -317,8 +317,10 @@ public: #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; Database* database(); - UnorderedMap loadRoomMegolmSessions(Room* room); - void saveMegolmSession(Room* room, QOlmInboundGroupSession* session); + UnorderedMap loadRoomMegolmSessions( + const Room* room); + void saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/converters.h b/lib/converters.h index 6515310a..5e3becb8 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -163,6 +163,14 @@ inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } +//! Use fromJson and use toLatin1()/toUtf8()/... to make QByteArray +//! +//! QJsonValue can only convert to QString and there's ambiguity whether +//! conversion to QByteArray should use (fast but very limited) toLatin1() or +//! (all encompassing and conforming to the JSON spec but slow) toUtf8(). +template <> +inline QByteArray fromJson(const QJsonValue& jv) = delete; + template <> inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 60d871ef..62856831 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -154,9 +154,9 @@ QString QOlmInboundGroupSession::olmSessionId() const { return m_olmSessionId; } -void QOlmInboundGroupSession::setOlmSessionId(const QString& olmSessionId) +void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId) { - m_olmSessionId = olmSessionId; + m_olmSessionId = newOlmSessionId; } QString QOlmInboundGroupSession::senderId() const diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 32112b97..13515434 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -44,7 +44,7 @@ public: //! The olm session that this session was received from. //! Required to get the device this session is from. QString olmSessionId() const; - void setOlmSessionId(const QString& setOlmSessionId); + void setOlmSessionId(const QString& newOlmSessionId); //! The sender of this session. QString senderId() const; diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 11d50768..2f26a1cb 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -33,7 +33,7 @@ CallInviteEvent::CallInviteEvent(const QJsonObject& obj) qCDebug(EVENTS) << "Call Invite event"; } -CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, +CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, const QString& sdp) : CallEventBase( typeId(), matrixTypeId(), callId, 0, diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 1b1f4f0f..5b4ca0df 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -13,10 +13,10 @@ public: explicit CallInviteEvent(const QJsonObject& obj); - explicit CallInviteEvent(const QString& callId, const int lifetime, + explicit CallInviteEvent(const QString& callId, int lifetime, const QString& sdp); - QUO_CONTENT_GETTER(int, lifetime) // FIXME: Omittable<>? + QUO_CONTENT_GETTER(int, lifetime) QString sdp() const { return contentPart("offer"_ls).value("sdp"_ls).toString(); diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index c4df7936..ed4c9440 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -16,7 +16,10 @@ public: QString algorithm() const { return contentPart("algorithm"_ls); } QString roomId() const { return contentPart(RoomIdKeyL); } QString sessionId() const { return contentPart("session_id"_ls); } - QString sessionKey() const { return contentPart("session_key"_ls); } + QByteArray sessionKey() const + { + return contentPart("session_key"_ls).toLatin1(); + } }; REGISTER_EVENT_TYPE(RoomKeyEvent) } // namespace Quotient 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()]; diff --git a/lib/room.h b/lib/room.h index 2e2ebf9a..6e6071f0 100644 --- a/lib/room.h +++ b/lib/room.h @@ -871,8 +871,8 @@ public Q_SLOTS: void inviteCall(const QString& callId, const int lifetime, const QString& sdp); void sendCallCandidates(const QString& callId, const QJsonArray& candidates); - [[deprecated("Lifetime argument is no more in use here; " - "use 2-arg Room::answerCall() instead")]] + //! \deprecated Lifetime argument is no more passed; use 2-arg + //! Room::answerCall() instead void answerCall(const QString& callId, int lifetime, const QString& sdp); void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); -- cgit v1.2.3 From f1ed7eec77f843de689c3f3540db73bb235b2164 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 14 May 2022 20:09:39 +0200 Subject: Quotest: test checkResource() --- quotest/quotest.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index b14442b9..1eed865f 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -764,6 +764,14 @@ TEST_IMPL(visitResources) clog << "Incorrect matrix.to representation:" << matrixToUrl.toStdString() << endl; } + const auto checkResult = checkResource(connection(), uriString); + if ((checkResult != UriResolved && uri.type() != Uri::NonMatrix) + || (uri.type() == Uri::NonMatrix + && checkResult != CouldNotResolve)) { + clog << "checkResource() returned incorrect result:" + << checkResult; + FAIL_TEST(); + } ud.visitResource(connection(), uriString); if (spy.count() != 1) { clog << "Wrong number of signal emissions (" << spy.count() -- 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(-) 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 3fcc0c5acb160364e819688cc67a8aaf814fafef Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 14 May 2022 20:51:05 +0200 Subject: Optimise #includes for QOlm* classes --- autotests/testolmutility.cpp | 2 ++ lib/connection.cpp | 15 ++++++++------- lib/e2ee/qolmaccount.cpp | 3 +++ lib/e2ee/qolmaccount.h | 6 ------ lib/e2ee/qolmsession.cpp | 7 ++++--- lib/e2ee/qolmsession.h | 7 ++----- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index b4532c8d..92b8b5b2 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -6,6 +6,8 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmutility.h" +#include + using namespace Quotient; void TestOlmUtility::canonicalJSON() diff --git a/lib/connection.cpp b/lib/connection.cpp index bd4d9251..511b64e2 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -35,16 +35,17 @@ #include "jobs/syncjob.h" #ifdef Quotient_E2EE_ENABLED -# include "e2ee/qolmaccount.h" -# include "e2ee/qolmutils.h" # include "database.h" +# include "e2ee/qolmaccount.h" # include "e2ee/qolminboundsession.h" +# include "e2ee/qolmsession.h" +# include "e2ee/qolmutils.h" -#if QT_VERSION_MAJOR >= 6 -# include -#else -# include -#endif +# if QT_VERSION_MAJOR >= 6 +# include +# else +# include +# endif #endif // Quotient_E2EE_ENABLED #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 476a60bd..af9eb1bf 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -5,6 +5,7 @@ #include "qolmaccount.h" #include "connection.h" +#include "e2ee/qolmsession.h" #include "e2ee/qolmutility.h" #include "e2ee/qolmutils.h" @@ -12,6 +13,8 @@ #include +#include + using namespace Quotient; QHash OneTimeKeys::curve25519() const diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 17f43f1a..08301998 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -9,18 +9,12 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolmmessage.h" -#include "e2ee/qolmsession.h" #include struct OlmAccount; namespace Quotient { -class QOlmSession; -class Connection; - -using QOlmSessionPtr = std::unique_ptr; - //! An olm account manages all cryptographic keys used on a device. //! \code{.cpp} //! const auto olmAccount = new QOlmAccount(this); diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index e575ff39..531a6696 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -3,10 +3,12 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "qolmsession.h" + #include "e2ee/qolmutils.h" #include "logging.h" + #include -#include +#include using namespace Quotient; @@ -247,5 +249,4 @@ std::variant QOlmSession::matchesInboundSessionFrom(const QStri QOlmSession::QOlmSession(OlmSession *session) : m_session(session) -{ -} +{} diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index f20c9837..4eb151b6 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -4,17 +4,14 @@ #pragma once -#include -#include // FIXME: OlmSession #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolmaccount.h" -namespace Quotient { +struct OlmSession; -class QOlmAccount; -class QOlmSession; +namespace Quotient { //! Either an outbound or inbound session for secure communication. class QUOTIENT_API QOlmSession -- cgit v1.2.3 From 9c8c33ab0b2138b45cbfe29f4be235f631730826 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 16 May 2022 10:41:36 +0200 Subject: Expected<> This is a minimal implementation along the lines of `std::expected<>` introduced in C++23; once compilers catch up with C++23 support, it may become simply a typedef of std::expected. There are no tests as yet; but the following commits will introduce QOlmExpected that would replace the current `std::variant` pattern used throughout `QOlm*` classes, automatically pulling Expected under the coverage of `QOlm*` unit tests. --- CMakeLists.txt | 1 + lib/expected.h | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 lib/expected.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 537d580e..404ba87c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,6 +118,7 @@ list(APPEND lib_SRCS lib/quotient_export.h lib/function_traits.h lib/function_traits.cpp lib/omittable.h + lib/expected.h lib/networkaccessmanager.h lib/networkaccessmanager.cpp lib/connectiondata.h lib/connectiondata.cpp lib/connection.h lib/connection.cpp diff --git a/lib/expected.h b/lib/expected.h new file mode 100644 index 00000000..c8b8fd64 --- /dev/null +++ b/lib/expected.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +namespace Quotient { + +//! \brief A minimal subset of std::expected from C++23 +template , bool> = true> +class Expected { +private: + template + using enable_if_constructible_t = std::enable_if_t< + std::is_constructible_v || std::is_constructible_v>; + +public: + using value_type = T; + using error_type = E; + + Expected() = default; + explicit Expected(const Expected&) = default; + explicit Expected(Expected&&) noexcept = default; + + template > + Expected(X&& x) + : data(std::forward(x)) + {} + + Expected& operator=(const Expected&) = default; + Expected& operator=(Expected&&) noexcept = default; + + template > + Expected& operator=(X&& x) + { + data = std::forward(x); + return *this; + } + + bool has_value() const { return std::holds_alternative(data); } + explicit operator bool() const { return has_value(); } + + const value_type& value() const& { return std::get(data); } + value_type& value() & { return std::get(data); } + value_type value() && { return std::get(std::move(data)); } + + const value_type& operator*() const& { return value(); } + value_type& operator*() & { return value(); } + + const value_type* operator->() const& { return std::get_if(&data); } + value_type* operator->() & { return std::get_if(&data); } + + template + T value_or(U&& fallback) const& + { + if (has_value()) + return value(); + return std::forward(fallback); + } + template + T value_or(U&& fallback) && + { + if (has_value()) + return value(); + return std::forward(fallback); + } + + const E& error() const& { return std::get(data); } + E& error() & { return std::get(data); } + +private: + std::variant data; +}; + +} // namespace Quotient -- cgit v1.2.3 From 1fe8dc00de4d9bb7072ec9677ec7f8e73e4fc769 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 15 May 2022 21:52:31 +0200 Subject: QOlmAccount::needsSave() shouldn't be const Making Qt signals const is an impossible commitment - once the signal is out, you can't control if any called slot will change the emitting class or not. The code compiles but const-ness is not preserved. --- lib/e2ee/qolmaccount.cpp | 5 +++-- lib/e2ee/qolmaccount.h | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index af9eb1bf..3339ce46 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -143,7 +143,7 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const return olm_account_max_number_of_one_time_keys(m_account); } -size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) { const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); QByteArray randomBuffer = getRandom(randomLength); @@ -196,7 +196,8 @@ QByteArray QOlmAccount::signOneTimeKey(const QString &key) const return sign(j.toJson(QJsonDocument::Compact)); } -std::optional QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const +std::optional QOlmAccount::removeOneTimeKeys( + const QOlmSessionPtr& session) { const auto error = olm_remove_one_time_keys(m_account, session->raw()); diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 08301998..1f36919a 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -24,7 +24,7 @@ class QUOTIENT_API QOlmAccount : public QObject Q_OBJECT public: QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); - ~QOlmAccount(); + ~QOlmAccount() override; //! Creates a new instance of OlmAccount. During the instantiation //! the Ed25519 fingerprint key pair and the Curve25519 identity key @@ -55,7 +55,7 @@ public: size_t maxNumberOfOneTimeKeys() const; //! Generates the supplied number of one time keys. - size_t generateOneTimeKeys(size_t numberOfKeys) const; + size_t generateOneTimeKeys(size_t numberOfKeys); //! Gets the OlmAccount's one time keys formatted as JSON. OneTimeKeys oneTimeKeys() const; @@ -97,7 +97,7 @@ public: OlmAccount *data(); Q_SIGNALS: - void needsSave() const; + void needsSave(); private: OlmAccount *m_account = nullptr; // owning -- cgit v1.2.3 From a3486fd0e9786c47564ce8173a2e4039135b10f9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 15 May 2022 22:08:09 +0200 Subject: Simplify QOlmSession::matchesInboundSession*() There's no particular use in letting `QOlmError` out, only to confirm that, well, `QOlmError` is just another form of no-match. --- autotests/testolmsession.cpp | 2 +- lib/connection.cpp | 3 +-- lib/e2ee/qolmsession.cpp | 43 +++++++++++++++++-------------------------- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 5436c392..2674b60b 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -45,7 +45,7 @@ void TestOlmSession::olmEncryptDecrypt() const auto encrypted = outboundSession->encrypt("Hello world!"); if (encrypted.type() == QOlmMessage::PreKey) { QOlmMessage m(encrypted); // clone - QVERIFY(std::get(inboundSession->matchesInboundSession(m))); + QVERIFY(inboundSession->matchesInboundSession(m)); } const auto decrypted = std::get(inboundSession->decrypt(encrypted)); diff --git a/lib/connection.cpp b/lib/connection.cpp index 511b64e2..955b5b1a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -226,8 +226,7 @@ public: { Q_ASSERT(message.type() == QOlmMessage::PreKey); for(auto& session : olmSessions[senderKey]) { - const auto matches = session->matchesInboundSessionFrom(senderKey, message); - if(std::holds_alternative(matches) && std::get(matches)) { + if (session->matchesInboundSessionFrom(senderKey, message)) { qCDebug(E2EE) << "Found inbound session"; const auto result = session->decrypt(message); if(std::holds_alternative(result)) { diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 531a6696..5ccbfd40 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -209,42 +209,33 @@ bool QOlmSession::hasReceivedMessage() const return olm_session_has_received_message(m_session); } -std::variant QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const +bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const { Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); QByteArray oneTimeKeyBuf(preKeyMessage.data()); - const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + const auto maybeMatches = + olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), + oneTimeKeyBuf.length()); - if (matchesResult == olm_error()) { + if (maybeMatches == olm_error()) { return lastError(m_session); } - switch (matchesResult) { - case 0: - return false; - case 1: - return true; - default: - return QOlmError::Unknown; - } + return maybeMatches == 1; } -std::variant QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const + +bool QOlmSession::matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const { const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), - oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - - if (error == olm_error()) { - return lastError(m_session); - } - switch (error) { - case 0: - return false; - case 1: - return true; - default: - return QOlmError::Unknown; - } + const auto maybeMatches = olm_matches_inbound_session_from( + m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + + if (maybeMatches == olm_error()) + qCWarning(E2EE) << "Error matching an inbound session:" + << olm_session_last_error(m_session); + return maybeMatches == 1; } QOlmSession::QOlmSession(OlmSession *session) -- 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()` --- autotests/testgroupsession.cpp | 14 ++-- autotests/testolmaccount.cpp | 5 +- autotests/testolmsession.cpp | 14 ++-- autotests/testolmutility.cpp | 5 +- lib/connection.cpp | 136 ++++++++++++++++++++------------------- lib/database.cpp | 32 ++++----- lib/e2ee/e2ee.h | 8 +++ lib/e2ee/qolmaccount.cpp | 20 +++--- lib/e2ee/qolmaccount.h | 21 +++--- lib/e2ee/qolminboundsession.cpp | 10 +-- lib/e2ee/qolminboundsession.h | 14 ++-- lib/e2ee/qolmoutboundsession.cpp | 18 +++--- lib/e2ee/qolmoutboundsession.h | 19 +++--- lib/e2ee/qolmsession.cpp | 22 +++++-- lib/e2ee/qolmsession.h | 35 +++++----- lib/e2ee/qolmutility.cpp | 14 ++-- lib/e2ee/qolmutility.h | 7 +- lib/room.cpp | 17 +++-- 18 files changed, 215 insertions(+), 196 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 2566669e..3c329a8a 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -16,11 +16,11 @@ void TestGroupSession::groupSessionPicklingValid() QVERIFY(QByteArray::fromBase64(ogsId).size() > 0); QCOMPARE(0, ogs->sessionMessageIndex()); - auto ogsPickled = std::get(ogs->pickle(Unencrypted {})); - auto ogs2 = std::get(QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {})); + auto ogsPickled = ogs->pickle(Unencrypted {}).value(); + auto ogs2 = QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {}).value(); QCOMPARE(ogsId, ogs2->sessionId()); - auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); + auto igs = QOlmInboundGroupSession::create(ogs->sessionKey().value()); const auto igsId = igs->sessionId(); // ID is valid base64? QVERIFY(QByteArray::fromBase64(igsId).size() > 0); @@ -29,22 +29,22 @@ void TestGroupSession::groupSessionPicklingValid() QCOMPARE(0, igs->firstKnownIndex()); auto igsPickled = igs->pickle(Unencrypted {}); - igs = std::get(QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {})); + igs = QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {}).value(); QCOMPARE(igsId, igs->sessionId()); } void TestGroupSession::groupSessionCryptoValid() { auto ogs = QOlmOutboundGroupSession::create(); - auto igs = QOlmInboundGroupSession::create(std::get(ogs->sessionKey())); + auto igs = QOlmInboundGroupSession::create(ogs->sessionKey().value()); QCOMPARE(ogs->sessionId(), igs->sessionId()); const auto plainText = QStringLiteral("Hello world!"); - const auto ciphertext = std::get(ogs->encrypt(plainText)); + const auto ciphertext = ogs->encrypt(plainText).value(); // ciphertext valid base64? QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0); - const auto decryptionResult = std::get>(igs->decrypt(ciphertext)); + const auto decryptionResult = igs->decrypt(ciphertext).value(); //// correct plaintext? QCOMPARE(plainText, decryptionResult.first); diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 9989665a..e31ff6d3 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -21,7 +21,7 @@ void TestOlmAccount::pickleUnpickledTest() QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); olmAccount.createNewAccount(); auto identityKeys = olmAccount.identityKeys(); - auto pickled = std::get(olmAccount.pickle(Unencrypted{})); + auto pickled = olmAccount.pickle(Unencrypted{}).value(); QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); olmAccount2.unpickle(pickled, Unencrypted{}); auto identityKeys2 = olmAccount2.identityKeys(); @@ -57,8 +57,7 @@ void TestOlmAccount::signatureValid() const auto identityKeys = olmAccount.identityKeys(); const auto ed25519Key = identityKeys.ed25519; const auto verify = utility.ed25519Verify(ed25519Key, message, signature); - QVERIFY(std::holds_alternative(verify)); - QVERIFY(std::get(verify) == true); + QVERIFY(verify.value_or(false)); } void TestOlmAccount::oneTimeKeysValid() diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 2674b60b..182659e7 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -20,8 +20,8 @@ std::pair createSessionPair() const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk"); const QByteArray identityKeyB("q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg"); const QByteArray oneTimeKeyB("oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ"); - auto outbound = std::get(accountA - .createOutboundSession(identityKeyB, oneTimeKeyB)); + auto outbound = + accountA.createOutboundSession(identityKeyB, oneTimeKeyB).value(); const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey @@ -29,7 +29,7 @@ std::pair createSessionPair() // We can't call QFail here because it's an helper function returning a value throw "Wrong first message type received, can't create session"; } - auto inbound = std::get(accountB.createInboundSession(preKey)); + auto inbound = accountB.createInboundSession(preKey).value(); return { std::move(inbound), std::move(outbound) }; } @@ -48,7 +48,7 @@ void TestOlmSession::olmEncryptDecrypt() QVERIFY(inboundSession->matchesInboundSession(m)); } - const auto decrypted = std::get(inboundSession->decrypt(encrypted)); + const auto decrypted = inboundSession->decrypt(encrypted).value(); QCOMPARE(decrypted, "Hello world!"); } @@ -56,11 +56,11 @@ void TestOlmSession::olmEncryptDecrypt() void TestOlmSession::correctSessionOrdering() { // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM - auto session1 = std::get(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{})); + auto session1 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{}).value(); // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0 - auto session2 = std::get(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {})); + auto session2 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {}).value(); // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME - auto session3 = std::get(QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{})); + auto session3 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{}).value(); const auto session1Id = session1->sessionId(); const auto session2Id = session2->sessionId(); diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 92b8b5b2..c1323533 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -81,7 +81,10 @@ void TestOlmUtility::verifySignedOneTimeKey() delete[](reinterpret_cast(utility)); QOlmUtility utility2; - auto res2 = std::get(utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg, signatureBuf1)); + auto res2 = + utility2 + .ed25519Verify(aliceOlm.identityKeys().ed25519, msg, signatureBuf1) + .value_or(false); //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); QCOMPARE(res2, true); diff --git a/lib/connection.cpp b/lib/connection.cpp index 955b5b1a..2528b70b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -212,82 +212,83 @@ public: void loadSessions() { olmSessions = q->database()->loadOlmSessions(q->picklingMode()); } - void saveSession(QOlmSessionPtr& session, const QString &senderKey) { - auto pickleResult = session->pickle(q->picklingMode()); - if (std::holds_alternative(pickleResult)) { - qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get(pickleResult); - return; - } - q->database()->saveOlmSession(senderKey, session->sessionId(), std::get(pickleResult), QDateTime::currentDateTime()); - } - - std::pair sessionDecryptPrekey(const QOlmMessage& message, - const QString& senderKey) + void saveSession(QOlmSession& session, const QString& senderKey) { - Q_ASSERT(message.type() == QOlmMessage::PreKey); - for(auto& session : olmSessions[senderKey]) { - if (session->matchesInboundSessionFrom(senderKey, message)) { - qCDebug(E2EE) << "Found inbound session"; - const auto result = session->decrypt(message); - if(std::holds_alternative(result)) { - q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); - return { std::get(result), session->sessionId() }; - } else { - qCDebug(E2EE) << "Failed to decrypt prekey message"; - return {}; - } - } - } - qCDebug(E2EE) << "Creating new inbound session"; - auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); - if(std::holds_alternative(newSessionResult)) { - qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get(newSessionResult); - return {}; - } - auto newSession = std::move(std::get(newSessionResult)); - auto error = olmAccount->removeOneTimeKeys(newSession); - if (error) { - qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); - } - const auto result = newSession->decrypt(message); - QString sessionId = newSession->sessionId(); - saveSession(newSession, senderKey); - olmSessions[senderKey].push_back(std::move(newSession)); - if(std::holds_alternative(result)) { - return { std::get(result), sessionId }; - } else { - qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; - return {}; - } + if (auto pickleResult = session.pickle(q->picklingMode())) + q->database()->saveOlmSession(senderKey, session.sessionId(), + *pickleResult, + QDateTime::currentDateTime()); + else + qCWarning(E2EE) << "Failed to pickle olm session. Error" + << pickleResult.error(); } - std::pair sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) + + template + std::pair doDecryptMessage(const QOlmSession& session, + const QOlmMessage& message, + FnT&& andThen) const { - Q_ASSERT(message.type() == QOlmMessage::General); - for(auto& session : olmSessions[senderKey]) { - const auto result = session->decrypt(message); - if(std::holds_alternative(result)) { - q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); - return { std::get(result), session->sessionId() }; - } + const auto expectedMessage = session.decrypt(message); + if (expectedMessage) { + const auto result = + std::make_pair(*expectedMessage, session.sessionId()); + andThen(); + return result; } - qCWarning(E2EE) << "Failed to decrypt message"; + const auto errorLine = message.type() == QOlmMessage::PreKey + ? "Failed to decrypt prekey message:" + : "Failed to decrypt message:"; + qCDebug(E2EE) << errorLine << expectedMessage.error(); return {}; } std::pair sessionDecryptMessage( const QJsonObject& personalCipherObject, const QByteArray& senderKey) { + const auto msgType = static_cast( + personalCipherObject.value(TypeKeyL).toInt(-1)); + if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) { + qCWarning(E2EE) << "Olm message has incorrect type" << msgType; + return {}; + } QOlmMessage message { - personalCipherObject.value(BodyKeyL).toString().toLatin1(), - static_cast( - personalCipherObject.value(TypeKeyL).toInt(-1)) + personalCipherObject.value(BodyKeyL).toString().toLatin1(), msgType }; - if (message.type() == QOlmMessage::PreKey) - return sessionDecryptPrekey(message, senderKey); - if (message.type() == QOlmMessage::General) - return sessionDecryptGeneral(message, senderKey); - qCWarning(E2EE) << "Olm message has incorrect type" << message.type(); - return {}; + for (const auto& session : olmSessions[senderKey]) + if (msgType == QOlmMessage::General + || session->matchesInboundSessionFrom(senderKey, message)) { + return doDecryptMessage(*session, message, [this, &session] { + q->database()->setOlmSessionLastReceived( + session->sessionId(), QDateTime::currentDateTime()); + }); + } + + if (msgType == QOlmMessage::General) { + qCWarning(E2EE) << "Failed to decrypt message"; + return {}; + } + + qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only + auto newSessionResult = + olmAccount->createInboundSessionFrom(senderKey, message); + if (!newSessionResult) { + qCWarning(E2EE) + << "Failed to create inbound session for" << senderKey + << "with error" << newSessionResult.error(); + return {}; + } + auto newSession = std::move(*newSessionResult); + auto error = olmAccount->removeOneTimeKeys(*newSession); + if (error) { + qWarning(E2EE) << "Failed to remove one time key for session" + << newSession->sessionId(); + // Keep going though + } + return doDecryptMessage( + *newSession, message, [this, &senderKey, &newSession] { + saveSession(*newSession, senderKey); + olmSessions[senderKey].push_back(std::move(newSession)); + }); } #endif @@ -2177,8 +2178,11 @@ void Connection::saveOlmAccount() { qCDebug(E2EE) << "Saving olm account"; #ifdef Quotient_E2EE_ENABLED - auto pickle = d->olmAccount->pickle(d->picklingMode); - d->database->setAccountPickle(std::get(pickle)); + if (const auto expectedPickle = d->olmAccount->pickle(d->picklingMode)) + d->database->setAccountPickle(*expectedPickle); + else + qCWarning(E2EE) << "Couldn't save Olm account pickle:" + << expectedPickle.error(); #endif } diff --git a/lib/database.cpp b/lib/database.cpp index 3189b3f1..e2e7acc9 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -184,12 +184,14 @@ UnorderedMap> Database::loadOlmSessions(con commit(); UnorderedMap> sessions; while (query.next()) { - auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative(session)) { - qCWarning(E2EE) << "Failed to unpickle olm session"; - continue; - } - sessions[query.value("senderKey").toString()].push_back(std::move(std::get(session))); + if (auto expectedSession = + QOlmSession::unpickle(query.value("pickle").toByteArray(), + picklingMode)) { + sessions[query.value("senderKey").toString()].emplace_back( + std::move(*expectedSession)); + } else + qCWarning(E2EE) + << "Failed to unpickle olm session:" << expectedSession.error(); } return sessions; } @@ -203,15 +205,15 @@ UnorderedMap Database::loadMegolmSessions(c commit(); UnorderedMap sessions; while (query.next()) { - auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative(session)) { - qCWarning(E2EE) << "Failed to unpickle megolm session"; - continue; - } - - sessions[query.value("sessionId").toString()] = std::move(std::get(session)); - sessions[query.value("sessionId").toString()]->setOlmSessionId(query.value("olmSessionId").toString()); - sessions[query.value("sessionId").toString()]->setSenderId(query.value("senderId").toString()); + if (auto expectedSession = QOlmInboundGroupSession::unpickle( + query.value("pickle").toByteArray(), picklingMode)) { + auto& sessionPtr = sessions[query.value("sessionId").toString()] = + std::move(*expectedSession); + sessionPtr->setOlmSessionId(query.value("olmSessionId").toString()); + sessionPtr->setSenderId(query.value("senderId").toString()); + } else + qCWarning(E2EE) << "Failed to unpickle megolm session:" + << expectedSession.error(); } return sessions; } diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 268cb525..8e433d60 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -6,6 +6,8 @@ #pragma once #include "converters.h" +#include "expected.h" +#include "qolmerrors.h" #include "quotient_common.h" #include @@ -55,6 +57,12 @@ using QOlmSessionPtr = std::unique_ptr; class QOlmInboundGroupSession; using QOlmInboundGroupSessionPtr = std::unique_ptr; +class QOlmOutboundGroupSession; +using QOlmOutboundGroupSessionPtr = std::unique_ptr; + +template +using QOlmExpected = Expected; + struct IdentityKeys { QByteArray curve25519; diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 3339ce46..72dddafb 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -73,7 +73,7 @@ void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) } } -std::variant QOlmAccount::pickle(const PicklingMode &mode) +QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) { const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); @@ -197,9 +197,9 @@ QByteArray QOlmAccount::signOneTimeKey(const QString &key) const } std::optional QOlmAccount::removeOneTimeKeys( - const QOlmSessionPtr& session) + const QOlmSession& session) { - const auto error = olm_remove_one_time_keys(m_account, session->raw()); + const auto error = olm_remove_one_time_keys(m_account, session.raw()); if (error == olm_error()) { return lastError(m_account); @@ -245,19 +245,19 @@ UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKey return new UploadKeysJob(keys, oneTimeKeysSigned); } -std::variant QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) +QOlmExpected QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSession(this, preKeyMessage); } -std::variant QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) +QOlmExpected QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); } -std::variant QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +QOlmExpected QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) { return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); } @@ -296,10 +296,6 @@ bool Quotient::ed25519VerifySignature(const QString& signingKey, QByteArray signingKeyBuf = signingKey.toUtf8(); QOlmUtility utility; auto signatureBuf = signature.toUtf8(); - auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); - if (std::holds_alternative(result)) { - return false; - } - - return std::get(result); + return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf) + .value_or(false); } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 1f36919a..ee2aa69d 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -5,11 +5,12 @@ #pragma once -#include "csapi/keys.h" #include "e2ee/e2ee.h" -#include "e2ee/qolmerrors.h" #include "e2ee/qolmmessage.h" -#include + +#include "csapi/keys.h" + +#include struct OlmAccount; @@ -38,7 +39,7 @@ public: void unpickle(QByteArray &pickled, const PicklingMode &mode); //! Serialises an OlmAccount to encrypted Base64. - std::variant pickle(const PicklingMode &mode); + QOlmExpected pickle(const PicklingMode &mode); //! Returns the account's public identity keys already formatted as JSON IdentityKeys identityKeys() const; @@ -73,22 +74,26 @@ public: DeviceKeys deviceKeys() const; //! Remove the one time key used to create the supplied session. - [[nodiscard]] std::optional removeOneTimeKeys(const QOlmSessionPtr &session) const; + [[nodiscard]] std::optional removeOneTimeKeys( + const QOlmSession& session); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param message An Olm pre-key message that was encrypted for this account. - std::variant createInboundSession(const QOlmMessage &preKeyMessage); + QOlmExpected createInboundSession( + const QOlmMessage& preKeyMessage); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param theirIdentityKey - The identity key of the Olm account that //! encrypted this Olm message. - std::variant createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); + QOlmExpected createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage); //! Creates an outbound session for sending messages to a specific /// identity and one time key. - std::variant createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + QOlmExpected createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey); void markKeysAsPublished(); diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 62856831..17f06205 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -70,7 +70,8 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const return pickledBuf; } -std::variant, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +QOlmExpected QOlmInboundGroupSession::unpickle( + const QByteArray& pickled, const PicklingMode& mode) { QByteArray pickledBuf = pickled; const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); @@ -85,7 +86,8 @@ std::variant, QOlmError> QOlmInboundGro return std::make_unique(groupSession); } -std::variant, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) +QOlmExpected> QOlmInboundGroupSession::decrypt( + const QByteArray& message) { // This is for capturing the output of olm_group_decrypt uint32_t messageIndex = 0; @@ -114,10 +116,10 @@ std::variant, QOlmError> QOlmInboundGroupSession::d QByteArray output(plaintextLen, '0'); std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); - return std::make_pair(QString(output), messageIndex); + return std::make_pair(output, messageIndex); } -std::variant QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +QOlmExpected QOlmInboundGroupSession::exportSession(uint32_t messageIndex) { const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); QByteArray keyBuf(keyLength, '0'); diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 13515434..1a9b4415 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -5,11 +5,8 @@ #pragma once #include "e2ee/e2ee.h" -#include "e2ee/qolmerrors.h" -#include "olm/olm.h" -#include -#include +#include namespace Quotient { @@ -27,14 +24,13 @@ public: QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant, QOlmError> - unpickle(const QByteArray& picked, const PicklingMode& mode); + static QOlmExpected unpickle( + const QByteArray& pickled, const PicklingMode& mode); //! Decrypts ciphertext received for this group session. - std::variant, QOlmError> decrypt( - const QByteArray& message); + QOlmExpected > decrypt(const QByteArray& message); //! Export the base64-encoded ratchet key for this session, at the given index, //! in a format which can be used by import. - std::variant exportSession(uint32_t messageIndex); + QOlmExpected exportSession(uint32_t messageIndex); //! Get the first message index we know how to decrypt. uint32_t firstKnownIndex() const; //! Get a base64-encoded identifier for this session. diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index da32417b..96bad344 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -13,8 +13,7 @@ QOlmError lastError(OlmOutboundGroupSession *session) { QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) : m_groupSession(session) -{ -} +{} QOlmOutboundGroupSession::~QOlmOutboundGroupSession() { @@ -22,7 +21,7 @@ QOlmOutboundGroupSession::~QOlmOutboundGroupSession() delete[](reinterpret_cast(m_groupSession)); } -std::unique_ptr QOlmOutboundGroupSession::create() +QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() { auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); @@ -45,7 +44,7 @@ std::unique_ptr QOlmOutboundGroupSession::create() return std::make_unique(olmOutboundGroupSession); } -std::variant QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mode) { QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); QByteArray key = toKey(mode); @@ -61,7 +60,7 @@ std::variant QOlmOutboundGroupSession::pickle(const Pickl return pickledBuf; } -std::variant, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +QOlmExpected QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); @@ -80,7 +79,7 @@ std::variant, QOlmError> QOlmOutboundG return std::make_unique(olmOutboundGroupSession); } -std::variant QOlmOutboundGroupSession::encrypt(const QString &plaintext) +QOlmExpected QOlmOutboundGroupSession::encrypt(const QString &plaintext) { QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); @@ -112,12 +111,13 @@ QByteArray QOlmOutboundGroupSession::sessionId() const return idBuffer; } -std::variant QOlmOutboundGroupSession::sessionKey() const +QOlmExpected QOlmOutboundGroupSession::sessionKey() const { const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); QByteArray keyBuffer(keyMaxLength, '0'); - const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); + const auto error = olm_outbound_group_session_key( + m_groupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength); if (error == olm_error()) { return lastError(m_groupSession); } diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 32ba2b3b..8058bbb1 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -4,10 +4,10 @@ #pragma once -#include "olm/olm.h" -#include "e2ee/qolmerrors.h" #include "e2ee/e2ee.h" + #include +#include namespace Quotient { @@ -19,15 +19,15 @@ public: ~QOlmOutboundGroupSession(); //! Creates a new instance of `QOlmOutboundGroupSession`. //! Throw OlmError on errors - static std::unique_ptr create(); + static QOlmOutboundGroupSessionPtr create(); //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); + QOlmExpected pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant, QOlmError> - unpickle(QByteArray& pickled, const PicklingMode& mode); + static QOlmExpected unpickle( + QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. - std::variant encrypt(const QString &plaintext); + QOlmExpected encrypt(const QString& plaintext); //! Get the current message index for this session. //! @@ -42,11 +42,10 @@ public: //! //! Each message is sent with a different ratchet key. This function returns the //! ratchet key that will be used for the next message. - std::variant sessionKey() const; + QOlmExpected sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); private: OlmOutboundGroupSession *m_groupSession; }; -using QOlmOutboundGroupSessionPtr = std::unique_ptr; -} +} // namespace Quotient diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 5ccbfd40..2b149aac 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -27,7 +27,9 @@ OlmSession* QOlmSession::create() return olm_session(new uint8_t[olm_session_size()]); } -std::variant QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) +QOlmExpected QOlmSession::createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from, + const QString& theirIdentityKey) { if (preKeyMessage.type() != QOlmMessage::PreKey) { qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; @@ -53,17 +55,22 @@ std::variant QOlmSession::createInbound(QOlmAccount * return std::make_unique(olmSession); } -std::variant QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) +QOlmExpected QOlmSession::createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage) { return createInbound(account, preKeyMessage); } -std::variant QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) +QOlmExpected QOlmSession::createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage) { return createInbound(account, preKeyMessage, true, theirIdentityKey); } -std::variant QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +QOlmExpected QOlmSession::createOutboundSession( + QOlmAccount* account, const QString& theirIdentityKey, + const QString& theirOneTimeKey) { auto *olmOutboundSession = create(); const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); @@ -89,7 +96,7 @@ std::variant QOlmSession::createOutboundSession(QOlmA return std::make_unique(olmOutboundSession); } -std::variant QOlmSession::pickle(const PicklingMode &mode) +QOlmExpected QOlmSession::pickle(const PicklingMode &mode) { QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); QByteArray key = toKey(mode); @@ -105,7 +112,8 @@ std::variant QOlmSession::pickle(const PicklingMode &mode return pickledBuf; } -std::variant QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +QOlmExpected QOlmSession::unpickle(const QByteArray& pickled, + const PicklingMode& mode) { QByteArray pickledBuf = pickled; auto *olmSession = create(); @@ -140,7 +148,7 @@ QOlmMessage QOlmSession::encrypt(const QString &plaintext) return QOlmMessage(messageBuf, messageType); } -std::variant QOlmSession::decrypt(const QOlmMessage &message) const +QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const { const auto messageType = message.type(); const auto ciphertext = message.toCiphertext(); diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index 4eb151b6..faae16ef 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -19,32 +19,31 @@ class QUOTIENT_API QOlmSession public: ~QOlmSession(); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::variant, QOlmError> - createInboundSession(QOlmAccount* account, const QOlmMessage& preKeyMessage); + static QOlmExpected createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage); - static std::variant, QOlmError> - createInboundSessionFrom(QOlmAccount* account, - const QString& theirIdentityKey, - const QOlmMessage& preKeyMessage); + static QOlmExpected createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage); - static std::variant, QOlmError> - createOutboundSession(QOlmAccount* account, const QString& theirIdentityKey, - const QString& theirOneTimeKey); + static QOlmExpected createOutboundSession( + QOlmAccount* account, const QString& theirIdentityKey, + const QString& theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. - std::variant pickle(const PicklingMode &mode); + QOlmExpected pickle(const PicklingMode &mode); //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant, QOlmError> unpickle( + static QOlmExpected unpickle( const QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. QOlmMessage encrypt(const QString &plaintext); - //! Decrypts a message using this session. Decoding is lossy, meaing if + //! Decrypts a message using this session. Decoding is lossy, meaning if //! the decrypted plaintext contains invalid UTF-8 symbols, they will //! be returned as `U+FFFD` (�). - std::variant decrypt(const QOlmMessage &message) const; + QOlmExpected decrypt(const QOlmMessage &message) const; //! Get a base64-encoded identifier for this session. QByteArray sessionId() const; @@ -56,11 +55,10 @@ public: bool hasReceivedMessage() const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSession( - const QOlmMessage& preKeyMessage) const; + bool matchesInboundSession(const QOlmMessage& preKeyMessage) const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant matchesInboundSessionFrom( + bool matchesInboundSessionFrom( const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const; friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) @@ -68,8 +66,7 @@ public: return lhs.sessionId() < rhs.sessionId(); } - friend bool operator<(const std::unique_ptr& lhs, - const std::unique_ptr& rhs) + friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs) { return *lhs < *rhs; } @@ -80,7 +77,7 @@ public: private: //! Helper function for creating new sessions and handling errors. static OlmSession* create(); - static std::variant, QOlmError> createInbound( + static QOlmExpected createInbound( QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); OlmSession* m_session; diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 9f09a37f..84559085 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "e2ee/qolmutility.h" -#include "olm/olm.h" -#include + +#include using namespace Quotient; @@ -40,8 +40,9 @@ QString QOlmUtility::sha256Utf8Msg(const QString &message) const return sha256Bytes(message.toUtf8()); } -std::variant QOlmUtility::ed25519Verify(const QByteArray &key, - const QByteArray &message, const QByteArray &signature) +QOlmExpected QOlmUtility::ed25519Verify(const QByteArray& key, + const QByteArray& message, + const QByteArray& signature) { QByteArray signatureBuf(signature.length(), '0'); std::copy(signature.begin(), signature.end(), signatureBuf.begin()); @@ -57,8 +58,5 @@ std::variant QOlmUtility::ed25519Verify(const QByteArray &key, return error; } - if (ret != 0) { - return false; - } - return true; + return !ret; // ret == 0 means success } diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index a12af49a..5f6bcdc5 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -4,15 +4,12 @@ #pragma once -#include -#include "e2ee/qolmerrors.h" +#include "e2ee/e2ee.h" struct OlmUtility; namespace Quotient { -class QOlmSession; - //! Allows you to make use of crytographic hashing via SHA-2 and //! verifying ed25519 signatures. class QUOTIENT_API QOlmUtility @@ -32,7 +29,7 @@ public: //! \param key QByteArray The public part of the ed25519 key that signed the message. //! \param message QByteArray The message that was signed. //! \param signature QByteArray The signature of the message. - std::variant ed25519Verify(const QByteArray &key, + QOlmExpected ed25519Verify(const QByteArray &key, const QByteArray &message, const QByteArray &signature); private: 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 56dbaab8fc8edc314ef7f7962697e7eb6ba71343 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 16 May 2022 14:15:39 +0200 Subject: Update autotests/testolmutility.cpp Co-authored-by: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> --- autotests/testolmutility.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index c1323533..5b67c805 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -87,7 +87,7 @@ void TestOlmUtility::verifySignedOneTimeKey() .value_or(false); //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); - QCOMPARE(res2, true); + QVERIFY(res2); } void TestOlmUtility::validUploadKeysRequest() -- cgit v1.2.3 From decc676f1e469dc26c80c33da64fad15805de8f2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 16 May 2022 17:35:58 +0200 Subject: expected.h: add a copyright notice [skip ci] --- lib/expected.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/expected.h b/lib/expected.h index c8b8fd64..7b9e7f1d 100644 --- a/lib/expected.h +++ b/lib/expected.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include -- cgit v1.2.3 From ff54bf2d0979dc6b9b3b77bba827ae7f3baa9f58 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 27 Feb 2022 19:15:16 +0100 Subject: Add constructor for creating roomkeyevents --- lib/events/roomkeyevent.cpp | 13 +++++++++++++ lib/events/roomkeyevent.h | 1 + 2 files changed, 14 insertions(+) diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 332be3f7..68962950 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -10,3 +10,16 @@ RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) if (roomId().isEmpty()) qCWarning(E2EE) << "Room key event has empty room id"; } + +RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString& sessionId, const QString& sessionKey, const QString& senderId) + : Event(typeId(), { + {"content", QJsonObject{ + {"algorithm", algorithm}, + {"room_id", roomId}, + {"session_id", sessionId}, + {"session_key", sessionKey}, + }}, + {"sender", senderId}, + {"type", "m.room_key"}, + }) +{} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index ed4c9440..2bda3086 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -12,6 +12,7 @@ public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) explicit RoomKeyEvent(const QJsonObject& obj); + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId); QString algorithm() const { return contentPart("algorithm"_ls); } QString roomId() const { return contentPart(RoomIdKeyL); } -- 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/connection.cpp | 30 +++++++- lib/connection.h | 9 ++- lib/room.cpp | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 226 insertions(+), 12 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2528b70b..a66a4168 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1332,7 +1332,7 @@ Connection::sendToDevices(const QString& eventType, [&jsonUser](const auto& deviceToEvents) { jsonUser.insert( deviceToEvents.first, - deviceToEvents.second.contentJson()); + deviceToEvents.second->contentJson()); }); }); return callApi(BackgroundRequest, eventType, @@ -2238,4 +2238,32 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) return query.next(); } +bool Connection::hasOlmSession(User* user, const QString& deviceId) const +{ + const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); + return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0; +} + +QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) +{ + //TODO be smarter about choosing a session; see e2ee impl guide + //TODO create session? + const auto& curveKey = curveKeyForUserDevice(user->id(), device); + QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); + auto result = d->olmSessions[curveKey][0]->encrypt(message); + return qMakePair(type, result.toCiphertext()); +} + +//TODO be more consistent with curveKey and identityKey +void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) +{ + auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); + if (std::holds_alternative(session)) { + //TODO something + qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + } + d->saveSession(std::get>(session), theirIdentityKey); + d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index b75bd5b5..afa4a657 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -24,6 +24,7 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -132,7 +133,7 @@ class QUOTIENT_API Connection : public QObject { public: using UsersToDevicesToEvents = - UnorderedMap>; + UnorderedMap>>; enum RoomVisibility { PublishRoom, @@ -321,6 +322,12 @@ public: const Room* room); void saveMegolmSession(const Room* room, const QOlmInboundGroupSession& session); + bool hasOlmSession(User* user, const QString& deviceId) const; + + //This currently assumes that an olm session with (user, device) exists + //TODO make this return an event? + QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); + void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; 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/connection.cpp | 11 ++++++++++- lib/connection.h | 5 +++++ lib/database.cpp | 41 +++++++++++++++++++++++++++++++++++++++- lib/database.h | 3 +++ lib/e2ee/qolmoutboundsession.cpp | 22 ++++++++++++++++++++- lib/e2ee/qolmoutboundsession.h | 9 +++++++++ lib/room.cpp | 23 +++++++++++----------- 7 files changed, 99 insertions(+), 15 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a66a4168..b11ec731 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2247,7 +2247,6 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { //TODO be smarter about choosing a session; see e2ee impl guide - //TODO create session? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); @@ -2266,4 +2265,14 @@ void Connection::createOlmSession(const QString& theirIdentityKey, const QString d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); } +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) +{ + return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode); +} + +void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data) +{ + d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index afa4a657..8bed55da 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -25,6 +25,7 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" +#include "e2ee/qolmoutboundsession.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -324,6 +325,10 @@ public: const QOlmInboundGroupSession& session); bool hasOlmSession(User* user, const QString& deviceId) const; + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); + void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); + + //This currently assumes that an olm session with (user, device) exists //TODO make this return an event? QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); diff --git a/lib/database.cpp b/lib/database.cpp index e2e7acc9..8cb3a9d1 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -86,7 +86,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, sessionId TEXT, pickle TEXT, creationTime TEXT, messageCount INTEGER);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); @@ -292,3 +292,42 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi execute(query); commit(); } + +void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) +{ + const auto pickle = session->pickle(picklingMode); + if (std::holds_alternative(pickle)) { + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); + deleteQuery.bindValue(":roomId", roomId); + deleteQuery.bindValue(":sessionId", session->sessionId()); + + auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); + insertQuery.bindValue(":roomId", roomId); + insertQuery.bindValue(":sessionId", session->sessionId()); + insertQuery.bindValue(":pickle", std::get(pickle)); + insertQuery.bindValue(":creationTime", session->creationTime()); + insertQuery.bindValue(":messageCount", session->messageCount()); + + transaction(); + execute(deleteQuery); + execute(insertQuery); + commit(); + } +} + +QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;")); + query.bindValue(":roomId", roomId); + execute(query); + if (query.next()) { + auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative(sessionResult)) { + auto session = std::move(std::get(sessionResult)); + session->setCreationTime(query.value("creationTime").toDateTime()); + session->setMessageCount(query.value("messageCount").toInt()); + return session; + } + } + return nullptr; +} diff --git a/lib/database.h b/lib/database.h index 08fe49f3..751ebd1d 100644 --- a/lib/database.h +++ b/lib/database.h @@ -8,6 +8,7 @@ #include #include "e2ee/e2ee.h" + namespace Quotient { class QUOTIENT_API Database : public QObject { @@ -34,6 +35,8 @@ public: std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); + void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); private: void migrateTo1(); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 96bad344..10b0c4de 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -66,7 +66,7 @@ QOlmExpected QOlmOutboundGroupSession::unpickle(QBy auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); QByteArray key = toKey(mode); const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), - pickled.data(), pickled.length()); + pickledBuf.data(), pickledBuf.length()); if (error == olm_error()) { return lastError(olmOutboundGroupSession); } @@ -123,3 +123,23 @@ QOlmExpected QOlmOutboundGroupSession::sessionKey() const } return keyBuffer; } + +int QOlmOutboundGroupSession::messageCount() const +{ + return m_messageCount; +} + +void QOlmOutboundGroupSession::setMessageCount(int messageCount) +{ + m_messageCount = messageCount; +} + +QDateTime QOlmOutboundGroupSession::creationTime() const +{ + return m_creationTime; +} + +void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime) +{ + m_creationTime = creationTime; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 8058bbb1..56b25974 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -26,6 +26,7 @@ public: //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( QByteArray& pickled, const PicklingMode& mode); + //! Encrypts a plaintext message using the session. QOlmExpected encrypt(const QString& plaintext); @@ -44,8 +45,16 @@ public: //! ratchet key that will be used for the next message. QOlmExpected sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); + + int messageCount() const; + void setMessageCount(int messageCount); + + QDateTime creationTime() const; + void setCreationTime(const QDateTime& creationTime); private: OlmOutboundGroupSession *m_groupSession; + int m_messageCount = 0; + QDateTime m_creationTime = QDateTime::currentDateTime(); }; } // namespace Quotient 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/connection.cpp | 1 + lib/database.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ lib/database.h | 9 +++++++++ lib/room.cpp | 53 ++++++++++++++++++++++++++++++++++------------------- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b11ec731..2a1b39f9 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2247,6 +2247,7 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { //TODO be smarter about choosing a session; see e2ee impl guide + //TODO do we need to save the olm session after sending a message? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); diff --git a/lib/database.cpp b/lib/database.cpp index 8cb3a9d1..4a28fd4c 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -13,6 +13,9 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" +#include "connection.h" +#include "user.h" +#include "room.h" using namespace Quotient; Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) @@ -91,6 +94,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -331,3 +335,43 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt } return nullptr; } + +void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) +{ + //TODO this better + auto connection = dynamic_cast(parent()); + transaction(); + for (const auto& user : devices.keys()) { + for (const auto& device : devices[user]) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user->id()); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device)); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); + } + } + commit(); +} + +QHash Database::devicesWithoutKey(Room* room, const QString &sessionId) +{ + auto connection = dynamic_cast(parent()); + QHash devices; + for (const auto& user : room->users()) {//TODO does this include invited & left? + devices[user->id()] = connection->devicesForUser(user); + } + + auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); + query.bindValue(":roomId", room->id()); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); + while (query.next()) { + devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString()); + } + return devices; +} diff --git a/lib/database.h b/lib/database.h index 751ebd1d..30f2f203 100644 --- a/lib/database.h +++ b/lib/database.h @@ -7,9 +7,14 @@ #include #include +#include + #include "e2ee/e2ee.h" namespace Quotient { +class User; +class Room; + class QUOTIENT_API Database : public QObject { Q_OBJECT @@ -38,6 +43,10 @@ public: QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); + // Returns a map User -> [Device] that have not received key yet + QHash devicesWithoutKey(Room* room, const QString &sessionId); + void setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index); + private: void migrateTo1(); void migrateTo2(); 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 --- autotests/CMakeLists.txt | 1 + autotests/testfilecrypto.cpp | 17 +++++++++++ autotests/testfilecrypto.h | 12 ++++++++ lib/eventitem.cpp | 10 +++++++ lib/eventitem.h | 3 ++ lib/events/encryptedfile.cpp | 26 +++++++++++++++-- lib/events/encryptedfile.h | 1 + lib/room.cpp | 67 +++++++++++++++++++++++++++++--------------- lib/room.h | 2 +- 9 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 autotests/testfilecrypto.cpp create mode 100644 autotests/testfilecrypto.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 671d6c08..c11901bf 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -18,4 +18,5 @@ if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testgroupsession) quotient_add_test(NAME testolmsession) quotient_add_test(NAME testolmutility) + quotient_add_test(NAME testfilecrypto) endif() diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp new file mode 100644 index 00000000..e6bec1fe --- /dev/null +++ b/autotests/testfilecrypto.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testfilecrypto.h" +#include "events/encryptedfile.h" +#include + +using namespace Quotient; +void TestFileCrypto::encryptDecryptData() +{ + QByteArray data = "ABCDEF"; + auto [file, cipherText] = EncryptedFile::encryptFile(data); + auto decrypted = file.decryptFile(cipherText); + QCOMPARE(data, decrypted); +} +QTEST_APPLESS_MAIN(TestFileCrypto) diff --git a/autotests/testfilecrypto.h b/autotests/testfilecrypto.h new file mode 100644 index 00000000..9096a8c7 --- /dev/null +++ b/autotests/testfilecrypto.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +class TestFileCrypto : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void encryptDecryptData(); +}; diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index a2d65d8d..302ae053 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -26,6 +26,16 @@ void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) setStatus(EventStatus::FileUploaded); } +void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) +{ + if (auto* rme = getAs()) { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([encryptedFile](EventContent::TypedBase& ec) { + ec.fileInfo()->file = encryptedFile; + }); + } +} + // Not exactly sure why but this helps with the linker not finding // Quotient::EventStatus::staticMetaObject when building Quaternion #include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index f04ef323..d8313736 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -9,6 +9,8 @@ #include #include +#include "events/encryptedfile.h" + namespace Quotient { namespace EventStatus { @@ -114,6 +116,7 @@ public: void setDeparted() { setStatus(EventStatus::Departed); } void setFileUploaded(const QUrl& remoteUrl); + void setEncryptedFile(const EncryptedFile& encryptedFile); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index d4a517bd..e90be428 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -8,6 +8,7 @@ #ifdef Quotient_E2EE_ENABLED #include #include +#include "e2ee/qolmutils.h" #endif using namespace Quotient; @@ -27,7 +28,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const { int length; auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_CIPHER_CTX_block_size(ctx) + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, @@ -44,7 +45,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const + length, &length); EVP_CIPHER_CTX_free(ctx); - return plaintext; + return plaintext.left(ciphertext.size()); } #else qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " @@ -53,6 +54,27 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const #endif } +std::pair EncryptedFile::encryptFile(const QByteArray &plainText) +{ + QByteArray k = getRandom(32); + auto kBase64 = k.toBase64(); + QByteArray iv = getRandom(16); + JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true}; + + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); + EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); + EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256).toBase64(); + auto ivBase64 = iv.toBase64(); + EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; + return {file, cipherText}; +} + void JsonObjectConverter::dumpTo(QJsonObject& jo, const EncryptedFile& pod) { diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index d0c4a030..2ce35086 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,6 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; + static std::pair encryptFile(const QByteArray &plainText); }; template <> 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())); diff --git a/lib/room.h b/lib/room.h index 6e6071f0..d5a8366a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = std::nullopt); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- 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/events/encryptedevent.cpp | 7 +++++++ lib/events/encryptedevent.h | 2 ++ lib/room.cpp | 3 +++ 3 files changed, 12 insertions(+) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 9d07a35f..3af3d6ff 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -61,3 +61,10 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const } return loadEvent(eventObject); } + +void EncryptedEvent::setRelation(const QJsonObject& relation) +{ + auto content = editJson()["content"_ls].toObject(); + content["m.relates_to"] = relation; + editJson()["content"] = content; +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 72efffd4..ddd5e415 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -56,6 +56,8 @@ public: QString deviceId() const { return contentPart(DeviceIdKeyL); } QString sessionId() const { return contentPart(SessionIdKeyL); } RoomEventPtr createDecrypted(const QString &decrypted) const; + + void setRelation(const QJsonObject& relation); }; REGISTER_EVENT_TYPE(EncryptedEvent) 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/connection.cpp | 12 ++++++++---- lib/connection.h | 3 +-- lib/database.cpp | 18 +++++++++++++++--- lib/database.h | 3 ++- lib/events/encryptedfile.cpp | 4 ++++ lib/room.cpp | 27 ++++++++++++++++----------- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2a1b39f9..82046d53 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -33,6 +33,7 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#include #ifdef Quotient_E2EE_ENABLED # include "database.h" @@ -2246,21 +2247,24 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { - //TODO be smarter about choosing a session; see e2ee impl guide - //TODO do we need to save the olm session after sending a message? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); + auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); + if (std::holds_alternative(pickle)) { + database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get(pickle)); + } else { + qCWarning(E2EE) << "Failed to pickle olm session."; + } return qMakePair(type, result.toCiphertext()); } -//TODO be more consistent with curveKey and identityKey void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) { auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); if (std::holds_alternative(session)) { - //TODO something qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + return; } d->saveSession(std::get>(session), theirIdentityKey); d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); diff --git a/lib/connection.h b/lib/connection.h index 8bed55da..5a1f1e5c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -329,8 +329,7 @@ public: void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); - //This currently assumes that an olm session with (user, device) exists - //TODO make this return an event? + //This assumes that an olm session with (user, device) exists QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED diff --git a/lib/database.cpp b/lib/database.cpp index 4a28fd4c..74b56a02 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" @@ -182,7 +183,7 @@ void Database::saveOlmSession(const QString& senderKey, const QString& sessionId UnorderedMap> Database::loadOlmSessions(const PicklingMode& picklingMode) { - auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions ORDER BY lastReceived DESC;")); transaction(); execute(query); commit(); @@ -338,7 +339,6 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) { - //TODO this better auto connection = dynamic_cast(parent()); transaction(); for (const auto& user : devices.keys()) { @@ -360,7 +360,7 @@ QHash Database::devicesWithoutKey(Room* room, const QStrin { auto connection = dynamic_cast(parent()); QHash devices; - for (const auto& user : room->users()) {//TODO does this include invited & left? + for (const auto& user : room->users()) { devices[user->id()] = connection->devicesForUser(user); } @@ -375,3 +375,15 @@ QHash Database::devicesWithoutKey(Room* room, const QStrin } return devices; } + +void Database::updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;")); + query.bindValue(":pickle", pickle); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + diff --git a/lib/database.h b/lib/database.h index 30f2f203..8ddd7b6d 100644 --- a/lib/database.h +++ b/lib/database.h @@ -32,7 +32,7 @@ public: QByteArray accountPickle(); void setAccountPickle(const QByteArray &pickle); void clear(); - void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp); + void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle, const QDateTime& timestamp); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); UnorderedMap loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); void saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId); @@ -42,6 +42,7 @@ public: void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); + void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); // Returns a map User -> [Device] that have not received key yet QHash devicesWithoutKey(Room* room, const QString &sessionId); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index e90be428..bb4e26c7 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -56,6 +56,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const std::pair EncryptedFile::encryptFile(const QByteArray &plainText) { +#ifdef Quotient_E2EE_ENABLED QByteArray k = getRandom(32); auto kBase64 = k.toBase64(); QByteArray iv = getRandom(16); @@ -73,6 +74,9 @@ std::pair EncryptedFile::encryptFile(const QByteArray auto ivBase64 = iv.toBase64(); EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; return {file, cipherText}; +#else + return {{}, {}}; +#endif } void JsonObjectConverter::dumpTo(QJsonObject& jo, 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 1b302abce0bfd9fb62cdc721bc7300dc61b1784f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 10 Mar 2022 21:47:51 +0100 Subject: Update lib/events/encryptedfile.h --- lib/events/encryptedfile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 2ce35086..022ac91e 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,7 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; - static std::pair encryptFile(const QByteArray &plainText); + static std::pair encryptFile(const QByteArray& plainText); }; template <> -- cgit v1.2.3 From 8af39e510e550d001e207bdc0177a1480f6ebcba Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 23 Mar 2022 20:42:28 +0100 Subject: Add database migration --- lib/database.cpp | 18 +++++++++++++++--- lib/database.h | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 74b56a02..d2d33006 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -34,6 +34,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa case 0: migrateTo1(); case 1: migrateTo2(); case 2: migrateTo3(); + case 3: migrateTo4(); } } @@ -90,12 +91,11 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, sessionId TEXT, pickle TEXT, creationTime TEXT, messageCount INTEGER);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); - execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -108,7 +108,7 @@ void Database::migrateTo2() //TODO remove this column again - we don't need it after all execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); - + // Add indexes for improving queries speed on larger database execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)")); execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)")); @@ -132,6 +132,18 @@ void Database::migrateTo3() commit(); } +void Database::migrateTo3() +{ + qCDebug(DATABASE) << "Migrating database to version 4"; + transaction(); + + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); + execute(QStringLiteral("PRAGMA user_version = 3;")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); diff --git a/lib/database.h b/lib/database.h index 8ddd7b6d..00002204 100644 --- a/lib/database.h +++ b/lib/database.h @@ -52,6 +52,7 @@ private: void migrateTo1(); void migrateTo2(); void migrateTo3(); + void migrateTo4(); QString m_matrixId; }; -- 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(-) 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/connection.cpp | 12 ++++++------ lib/database.cpp | 10 +++++----- lib/e2ee/qolmoutboundsession.cpp | 2 +- lib/e2ee/qolmoutboundsession.h | 2 +- lib/room.cpp | 16 ++++++++-------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 82046d53..a5615f64 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2251,8 +2251,8 @@ QPair Connection::olmEncryptMessage(User* user, c QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); - if (std::holds_alternative(pickle)) { - database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get(pickle)); + if (pickle) { + database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle); } else { qCWarning(E2EE) << "Failed to pickle olm session."; } @@ -2262,12 +2262,12 @@ QPair Connection::olmEncryptMessage(User* user, c void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) { auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); - if (std::holds_alternative(session)) { - qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + if (!session) { + qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << session.error(); return; } - d->saveSession(std::get>(session), theirIdentityKey); - d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); + d->saveSession(**session, theirIdentityKey); + d->olmSessions[theirIdentityKey].push_back(std::move(*session)); } QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) diff --git a/lib/database.cpp b/lib/database.cpp index d2d33006..87275e1f 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -132,7 +132,7 @@ void Database::migrateTo3() commit(); } -void Database::migrateTo3() +void Database::migrateTo4() { qCDebug(DATABASE) << "Migrating database to version 4"; transaction(); @@ -313,7 +313,7 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) { const auto pickle = session->pickle(picklingMode); - if (std::holds_alternative(pickle)) { + if (pickle) { auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); deleteQuery.bindValue(":roomId", roomId); deleteQuery.bindValue(":sessionId", session->sessionId()); @@ -321,7 +321,7 @@ void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const Pic auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); insertQuery.bindValue(":roomId", roomId); insertQuery.bindValue(":sessionId", session->sessionId()); - insertQuery.bindValue(":pickle", std::get(pickle)); + insertQuery.bindValue(":pickle", pickle.value()); insertQuery.bindValue(":creationTime", session->creationTime()); insertQuery.bindValue(":messageCount", session->messageCount()); @@ -339,8 +339,8 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt execute(query); if (query.next()) { auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative(sessionResult)) { - auto session = std::move(std::get(sessionResult)); + if (sessionResult) { + auto session = std::move(*sessionResult); session->setCreationTime(query.value("creationTime").toDateTime()); session->setMessageCount(query.value("messageCount").toInt()); return session; diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 10b0c4de..76188d08 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -60,7 +60,7 @@ QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mo return pickledBuf; } -QOlmExpected QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +QOlmExpected QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 56b25974..c20613d3 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -25,7 +25,7 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( - QByteArray& pickled, const PicklingMode& mode); + const QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. QOlmExpected encrypt(const QString& plaintext); 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 c671867a0a3e2a6ad0e7ae6e93fa09467c06188f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 18 May 2022 22:02:50 +0200 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/connection.cpp | 7 +++---- lib/connection.h | 2 +- lib/database.cpp | 3 +-- lib/events/encryptedfile.cpp | 2 +- lib/room.h | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a5615f64..66e21a2a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -33,7 +33,6 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" -#include #ifdef Quotient_E2EE_ENABLED # include "database.h" @@ -2242,7 +2241,7 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) bool Connection::hasOlmSession(User* user, const QString& deviceId) const { const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); - return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0; + return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) @@ -2254,9 +2253,9 @@ QPair Connection::olmEncryptMessage(User* user, c if (pickle) { database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle); } else { - qCWarning(E2EE) << "Failed to pickle olm session."; + qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } - return qMakePair(type, result.toCiphertext()); + return { type, result.toCiphertext() }; } void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) diff --git a/lib/connection.h b/lib/connection.h index 5a1f1e5c..5b266aad 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -134,7 +134,7 @@ class QUOTIENT_API Connection : public QObject { public: using UsersToDevicesToEvents = - UnorderedMap>>; + UnorderedMap>; enum RoomVisibility { PublishRoom, diff --git a/lib/database.cpp b/lib/database.cpp index 87275e1f..99c6f358 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" @@ -140,7 +139,7 @@ void Database::migrateTo4() execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); - execute(QStringLiteral("PRAGMA user_version = 3;")); + execute(QStringLiteral("PRAGMA user_version = 4;")); commit(); } diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index bb4e26c7..d35ee28f 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -75,7 +75,7 @@ std::pair EncryptedFile::encryptFile(const QByteArray EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; return {file, cipherText}; #else - return {{}, {}}; + return {}; #endif } diff --git a/lib/room.h b/lib/room.h index d5a8366a..b1201a6c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = std::nullopt); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = none); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- cgit v1.2.3 From 9c4cc1b9b065765843c81a0c555b3afa5122b61e Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 18 May 2022 22:05:48 +0200 Subject: Update lib/events/encryptedevent.cpp Co-authored-by: Alexey Rusakov --- lib/events/encryptedevent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 3af3d6ff..c97ccc16 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -64,7 +64,7 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const void EncryptedEvent::setRelation(const QJsonObject& relation) { - auto content = editJson()["content"_ls].toObject(); + auto content = contentJson(); content["m.relates_to"] = relation; editJson()["content"] = content; } -- 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/connection.cpp | 12 ++++++------ lib/connection.h | 6 +++--- lib/database.cpp | 23 +++++++---------------- lib/database.h | 6 +++--- lib/room.cpp | 51 ++++++++++++++++++++++++++++----------------------- 5 files changed, 47 insertions(+), 51 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 66e21a2a..dba18cb1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2214,9 +2214,9 @@ void Connection::saveMegolmSession(const Room* room, session.senderId(), session.olmSessionId()); } -QStringList Connection::devicesForUser(User* user) const +QStringList Connection::devicesForUser(const QString& userId) const { - return d->deviceKeys[user->id()].keys(); + return d->deviceKeys[userId].keys(); } QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const @@ -2238,15 +2238,15 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) return query.next(); } -bool Connection::hasOlmSession(User* user, const QString& deviceId) const +bool Connection::hasOlmSession(const QString& user, const QString& deviceId) const { - const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); + const auto& curveKey = curveKeyForUserDevice(user, deviceId); return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } -QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) +QPair Connection::olmEncryptMessage(const QString& user, const QString& device, const QByteArray& message) { - const auto& curveKey = curveKeyForUserDevice(user->id(), device); + const auto& curveKey = curveKeyForUserDevice(user, device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); diff --git a/lib/connection.h b/lib/connection.h index 5b266aad..f8744752 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -323,14 +323,14 @@ public: const Room* room); void saveMegolmSession(const Room* room, const QOlmInboundGroupSession& session); - bool hasOlmSession(User* user, const QString& deviceId) const; + bool hasOlmSession(const QString& user, const QString& deviceId) const; QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); //This assumes that an olm session with (user, device) exists - QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); + QPair olmEncryptMessage(const QString& userId, const QString& device, const QByteArray& message); void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; @@ -694,7 +694,7 @@ public Q_SLOTS: PicklingMode picklingMode() const; QJsonObject decryptNotification(const QJsonObject ¬ification); - QStringList devicesForUser(User* user) const; + QStringList devicesForUser(const QString& user) const; QString curveKeyForUserDevice(const QString &user, const QString& device) const; QString edKeyForUserDevice(const QString& user, const QString& device) const; bool isKnownCurveKey(const QString& user, const QString& curveKey); diff --git a/lib/database.cpp b/lib/database.cpp index 99c6f358..3255e5e7 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -13,9 +13,7 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" -#include "connection.h" -#include "user.h" -#include "room.h" +#include "e2ee/qolmoutboundsession.h" using namespace Quotient; Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) @@ -348,17 +346,16 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt return nullptr; } -void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) +void Database::setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index) { - auto connection = dynamic_cast(parent()); transaction(); for (const auto& user : devices.keys()) { - for (const auto& device : devices[user]) { + for (const auto& [device, curveKey] : devices[user]) { auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); query.bindValue(":roomId", roomId); - query.bindValue(":userId", user->id()); + query.bindValue(":userId", user); query.bindValue(":deviceId", device); - query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device)); + query.bindValue(":identityKey", curveKey); query.bindValue(":sessionId", sessionId); query.bindValue(":i", index); execute(query); @@ -367,16 +364,10 @@ void Database::setDevicesReceivedKey(const QString& roomId, QHash Database::devicesWithoutKey(Room* room, const QString &sessionId) +QHash Database::devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId) { - auto connection = dynamic_cast(parent()); - QHash devices; - for (const auto& user : room->users()) { - devices[user->id()] = connection->devicesForUser(user); - } - auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); - query.bindValue(":roomId", room->id()); + query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); transaction(); execute(query); diff --git a/lib/database.h b/lib/database.h index 00002204..8bef332f 100644 --- a/lib/database.h +++ b/lib/database.h @@ -44,9 +44,9 @@ public: void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); - // Returns a map User -> [Device] that have not received key yet - QHash devicesWithoutKey(Room* room, const QString &sessionId); - void setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index); + // Returns a map UserId -> [DeviceId] that have not received key yet + QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); + void setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index); private: void migrateTo1(); 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/database.cpp | 22 ++++++++++------------ lib/database.h | 2 +- lib/room.cpp | 4 ++-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 3255e5e7..0119b35c 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -346,20 +346,18 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt return nullptr; } -void Database::setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index) +void Database::setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index) { transaction(); - for (const auto& user : devices.keys()) { - for (const auto& [device, curveKey] : devices[user]) { - auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); - query.bindValue(":roomId", roomId); - query.bindValue(":userId", user); - query.bindValue(":deviceId", device); - query.bindValue(":identityKey", curveKey); - query.bindValue(":sessionId", sessionId); - query.bindValue(":i", index); - execute(query); - } + for (const auto& [user, device, curveKey] : devices) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", curveKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); } commit(); } diff --git a/lib/database.h b/lib/database.h index 8bef332f..ef251d66 100644 --- a/lib/database.h +++ b/lib/database.h @@ -46,7 +46,7 @@ public: // Returns a map UserId -> [DeviceId] that have not received key yet QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); - void setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index); + void setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index); private: void migrateTo1(); 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(-) 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 ++-- lib/room.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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, diff --git a/lib/room.h b/lib/room.h index b1201a6c..c3bdc4a0 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = none); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- 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(-) 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 5df53b8d5c8b21228ecf9938330dd4d85d3de6af Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 16:01:07 +0200 Subject: Document devices tuple --- lib/database.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/database.h b/lib/database.h index ef251d66..45348c8d 100644 --- a/lib/database.h +++ b/lib/database.h @@ -46,6 +46,7 @@ public: // Returns a map UserId -> [DeviceId] that have not received key yet QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); + // 'devices' contains tuples {userId, deviceId, curveKey} void setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index); private: -- 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(-) 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 a78bf8528fc38f7e1a3f6f912ef3d915eb1b5f29 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 19 May 2022 16:16:19 +0200 Subject: Use Clang 12 for LGTM now that it runs on focal --- .lgtm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lgtm.yml b/.lgtm.yml index a01e1de9..9cf3583d 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -15,4 +15,4 @@ extraction: # - cmake --build build # - popd configure: - command: "CXX=clang++-9 cmake . -GNinja" # -DOlm_DIR=olm/build" + command: "CXX=clang++-12 cmake . -GNinja" # -DOlm_DIR=olm/build" -- cgit v1.2.3 From 10197b03cc354f69b9d3cc028d00cf3df00ca91f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 19 May 2022 21:21:31 +0200 Subject: README: drop defunct Merge chance badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 71f2d04c..f2bfcb9e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) ![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/quotient-im/libQuotient.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quotient-im/libQuotient/context:cpp) -[![merge-chance-badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fmerge-chance.info%2Fbadge%3Frepo%3Dquotient-im/libquotient)](https://merge-chance.info/target?repo=quotient-im/libquotient) The Quotient project aims to produce a Qt5-based SDK to develop applications for [Matrix](https://matrix.org). libQuotient is a library that enables client -- cgit v1.2.3 From a8076b9a2394150e11381dc8fc2e3af2bbd03f39 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 21:58:54 +0200 Subject: Fix cipher text buffer initialization --- lib/events/encryptedfile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index d35ee28f..9cc3a0c8 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -64,7 +64,7 @@ std::pair EncryptedFile::encryptFile(const QByteArray int length; auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1); + QByteArray cipherText(plainText.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); -- cgit v1.2.3 From 7787df0119e52621a03b3d4ad30d27165191fd77 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 22:33:03 +0200 Subject: Add function to check if e2ee is supported --- lib/util.cpp | 9 +++++++++ lib/util.h | 1 + 2 files changed, 10 insertions(+) diff --git a/lib/util.cpp b/lib/util.cpp index 03ebf325..359b2959 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -135,3 +135,12 @@ int Quotient::patchVersion() { return Quotient_VERSION_PATCH; } + +bool Quotient::encryptionSupported() +{ +#ifdef Quotient_E2EE_ENABLED + return true; +#else + return false; +#endif +} diff --git a/lib/util.h b/lib/util.h index 3910059b..5dd69d74 100644 --- a/lib/util.h +++ b/lib/util.h @@ -199,4 +199,5 @@ QUOTIENT_API QString versionString(); QUOTIENT_API int majorVersion(); QUOTIENT_API int minorVersion(); QUOTIENT_API int patchVersion(); +QUOTIENT_API bool encryptionSupported(); } // namespace Quotient -- cgit v1.2.3 From 44f34c60fe1f1dde859655bbda86221b6cec4811 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 20 May 2022 12:41:06 +0200 Subject: Truncate ciphertext buffer to actual size during file encryption The ciphertext for AES CTR is exactly as large as the plaintext (not necessarily a multiple of the blocksize!). By truncating the ciphertext, we do not send bytes that will be decrypted to gibberish. As a side node, we probably do not need to initialize the ciphertext buffer larger than the plaintext size at all, but the OpenSSL docs are a bit vague about that. --- autotests/testfilecrypto.cpp | 4 +++- lib/events/encryptedfile.cpp | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp index e6bec1fe..5d549b89 100644 --- a/autotests/testfilecrypto.cpp +++ b/autotests/testfilecrypto.cpp @@ -12,6 +12,8 @@ void TestFileCrypto::encryptDecryptData() QByteArray data = "ABCDEF"; auto [file, cipherText] = EncryptedFile::encryptFile(data); auto decrypted = file.decryptFile(cipherText); - QCOMPARE(data, decrypted); + QCOMPARE(cipherText.size(), data.size()); + QCOMPARE(decrypted.size(), data.size()); + QCOMPARE(decrypted, data); } QTEST_APPLESS_MAIN(TestFileCrypto) diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index 9cc3a0c8..140dca7f 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -67,6 +67,7 @@ std::pair EncryptedFile::encryptFile(const QByteArray QByteArray cipherText(plainText.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); + cipherText.resize(length); EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); EVP_CIPHER_CTX_free(ctx); -- cgit v1.2.3 From 59f2b60835752fc87e75f456145d21cc5f77a433 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Fri, 20 May 2022 20:33:12 +0200 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- autotests/testfilecrypto.cpp | 1 + lib/events/encryptedfile.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp index 5d549b89..f9212376 100644 --- a/autotests/testfilecrypto.cpp +++ b/autotests/testfilecrypto.cpp @@ -12,6 +12,7 @@ void TestFileCrypto::encryptDecryptData() QByteArray data = "ABCDEF"; auto [file, cipherText] = EncryptedFile::encryptFile(data); auto decrypted = file.decryptFile(cipherText); + // AES CTR produces ciphertext of the same size as the original QCOMPARE(cipherText.size(), data.size()); QCOMPARE(decrypted.size(), data.size()); QCOMPARE(decrypted, data); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index 140dca7f..33ebb514 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -64,10 +64,10 @@ std::pair EncryptedFile::encryptFile(const QByteArray int length; auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray cipherText(plainText.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); + const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); + QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); - cipherText.resize(length); EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); EVP_CIPHER_CTX_free(ctx); -- cgit v1.2.3 From 617514cf9da4d444c285ebb27ff1c7fd034a484f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 20 May 2022 22:24:41 +0200 Subject: Adapt update-api target to matrix-doc split --- .github/workflows/ci.yml | 4 ++-- CMakeLists.txt | 8 ++++---- README.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b704b3b9..9b9383db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,11 +179,11 @@ jobs: if: matrix.update-api working-directory: ${{ runner.workspace }} run: | - git clone https://github.com/matrix-org/matrix-doc.git + git clone https://github.com/quotient-im/matrix-spec.git git clone --recursive https://github.com/KitsuneRal/gtad.git cmake -S gtad -B build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF cmake --build build/gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc \ + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_SPEC_PATH=${{ runner.workspace }}/matrix-spec \ -DGTAD_PATH=${{ runner.workspace }}/build/gtad/gtad" \ >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV diff --git a/CMakeLists.txt b/CMakeLists.txt index 404ba87c..e3415816 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,20 +198,20 @@ set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) set(API_GENERATION_ENABLED 0) -if (GTAD_PATH AND MATRIX_DOC_PATH) +if (GTAD_PATH AND MATRIX_SPEC_PATH) # REALPATH resolves ~ (home directory) while PROGRAM doesn't get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) get_filename_component(ABS_GTAD_PATH "${ABS_GTAD_PATH}" PROGRAM PROGRAM_ARGS GTAD_ARGS) if (EXISTS ${ABS_GTAD_PATH}) - get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/data/api" REALPATH) + get_filename_component(ABS_API_DEF_PATH "${MATRIX_SPEC_PATH}/data/api" REALPATH) if (NOT IS_DIRECTORY ${ABS_API_DEF_PATH}) # Check the old place of API files - get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) + get_filename_component(ABS_API_DEF_PATH "${MATRIX_SPEC_PATH}/api" REALPATH) endif () if (IS_DIRECTORY ${ABS_API_DEF_PATH}) set(API_GENERATION_ENABLED 1) else () - message( WARNING "${MATRIX_DOC_PATH} doesn't seem to point to a valid matrix-doc repo; disabling API stubs generation") + message( WARNING "${MATRIX_SPEC_PATH} doesn't seem to point to a valid matrix-doc repo; disabling API stubs generation") endif () else (EXISTS ${ABS_GTAD_PATH}) message( WARNING "${GTAD_PATH} is not executable; disabling API stubs generation") diff --git a/README.md b/README.md index f2bfcb9e..15f1fcd7 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ the standard variables coming with CMake. On top of them, Quotient introduces: Quotient and Quotient-dependent (if it uses `find_package(Quotient 0.6)`) code; so you can use `#ifdef Quotient_E2EE_ENABLED` to guard the code using E2EE parts of Quotient. -- `MATRIX_DOC_PATH` and `GTAD_PATH` - these two variables are used to point +- `MATRIX_SPEC_PATH` and `GTAD_PATH` - these two variables are used to point CMake to the directory with the matrix-doc repository containing API files and to a GTAD binary. These two are used to generate C++ files from Matrix Client-Server API description made in OpenAPI notation. This is not needed -- cgit v1.2.3 From 753037cfc0d72a49144d30e9111ca21f01b81afd Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 20 May 2022 22:33:37 +0200 Subject: Provide backwards compatibility for MATRIX_SPEC_PATH --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e3415816..ce950ea3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,6 +198,9 @@ set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) set(API_GENERATION_ENABLED 0) +if (NOT MATRIX_SPEC_PATH AND MATRIX_DOC_PATH) + set(MATRIX_SPEC_PATH ${MATRIX_DOC_PATH}) +endif() if (GTAD_PATH AND MATRIX_SPEC_PATH) # REALPATH resolves ~ (home directory) while PROGRAM doesn't get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) -- cgit v1.2.3 From 4e127d5f74c56b1e366de3e8afb7f07700a566b4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 21 May 2022 16:29:00 +0200 Subject: Use branch of matrix-spec --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b9383db..bea9f436 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,6 +180,9 @@ jobs: working-directory: ${{ runner.workspace }} run: | git clone https://github.com/quotient-im/matrix-spec.git + pushd matrix-spec + git checkout fixcompilation + popd git clone --recursive https://github.com/KitsuneRal/gtad.git cmake -S gtad -B build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF cmake --build build/gtad -- cgit v1.2.3 From 946cd4cb73f95526aa09777afc2a493610d9696d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 27 May 2022 21:03:31 +0200 Subject: Load and store accounts in the keychain --- CMakeLists.txt | 8 +++--- lib/accountregistry.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++++++-- lib/accountregistry.h | 20 +++++++++++++++ lib/connection.cpp | 39 ++++++++++++++++++++++++---- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 404ba87c..7900235c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,8 @@ find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModul get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") +find_package(${Qt}Keychain REQUIRED) + if (${PROJECT_NAME}_ENABLE_E2EE) find_package(${Qt} ${QtMinVersion} REQUIRED Sql) find_package(Olm 3.1.3 REQUIRED) @@ -108,7 +110,6 @@ if (${PROJECT_NAME}_ENABLE_E2EE) if (OpenSSL_FOUND) message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}") endif() - find_package(${Qt}Keychain REQUIRED) endif() @@ -309,14 +310,13 @@ if (${PROJECT_NAME}_ENABLE_E2EE) target_link_libraries(${PROJECT_NAME} Olm::Olm OpenSSL::Crypto OpenSSL::SSL - ${Qt}::Sql - ${QTKEYCHAIN_LIBRARIES}) + ${Qt}::Sql) set(FIND_DEPS "find_dependency(Olm) find_dependency(OpenSSL) find_dependency(${Qt}Sql)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES}) if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 616b54b4..9b6114d9 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -5,7 +5,13 @@ #include "accountregistry.h" #include "connection.h" +#include +#if QT_VERSION_MAJOR >= 6 +# include +#else +# include +#endif using namespace Quotient; void AccountRegistry::add(Connection* a) @@ -15,6 +21,7 @@ void AccountRegistry::add(Connection* a) beginInsertRows(QModelIndex(), size(), size()); push_back(a); endInsertRows(); + Q_EMIT accountCountChanged(); } void AccountRegistry::drop(Connection* a) @@ -54,8 +61,6 @@ QHash AccountRegistry::roleNames() const return { { AccountRole, "connection" } }; } - - Connection* AccountRegistry::get(const QString& userId) { for (const auto &connection : *this) { @@ -64,3 +69,61 @@ Connection* AccountRegistry::get(const QString& userId) } return nullptr; } + +QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const QString& userId) +{ + qCDebug(MAIN) << "Reading access token from keychain for" << userId; + auto job = new QKeychain::ReadPasswordJob(qAppName(), this); + job->setKey(userId); + + connect(job, &QKeychain::Job::finished, this, [job]() { + if (job->error() == QKeychain::Error::NoError) { + return; + } + //TODO error handling + }); + job->start(); + + return job; +} + +void AccountRegistry::invokeLogin() +{ + const auto accounts = SettingsGroup("Accounts").childGroups(); + for (const auto& accountId : accounts) { + AccountSettings account{accountId}; + m_accountsLoading += accountId; + Q_EMIT accountsLoadingChanged(); + + if (!account.homeserver().isEmpty()) { + auto accessTokenLoadingJob = loadAccessTokenFromKeychain(account.userId()); + connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob]() { + AccountSettings account{accountId}; + if (accessTokenLoadingJob->error() != QKeychain::Error::NoError) { + //TODO error handling + return; + } + + auto connection = new Connection(account.homeserver()); + connect(connection, &Connection::connected, this, [connection] { + connection->loadState(); + connection->setLazyLoading(true); + + connection->syncLoop(); + }); + connect(connection, &Connection::loginError, this, [](const QString& error, const QString&) { + //TODO error handling + }); + connect(connection, &Connection::networkError, this, [](const QString& error, const QString&, int, int) { + //TODO error handling + }); + connection->assumeIdentity(account.userId(), accessTokenLoadingJob->binaryData(), account.deviceId()); + }); + } + } +} + +QStringList AccountRegistry::accountsLoading() const +{ + return m_accountsLoading; +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 2f6dffdf..ab337303 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -5,15 +5,25 @@ #pragma once #include "quotient_export.h" +#include "settings.h" #include +namespace QKeychain { +class ReadPasswordJob; +} + namespace Quotient { class Connection; class QUOTIENT_API AccountRegistry : public QAbstractListModel, private QVector { Q_OBJECT + /// Number of accounts that are currently fully loaded + Q_PROPERTY(int accountCount READ rowCount NOTIFY accountCountChanged) + /// List of accounts that are currently in some stage of being loaded (Reading token from keychain, trying to contact server, etc). + /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded + Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged) public: using const_iterator = QVector::const_iterator; using const_reference = QVector::const_reference; @@ -52,6 +62,16 @@ public: [[nodiscard]] int rowCount( const QModelIndex& parent = QModelIndex()) const override; [[nodiscard]] QHash roleNames() const override; + + QStringList accountsLoading() const; + + void invokeLogin(); +Q_SIGNALS: + void accountCountChanged(); + void accountsLoadingChanged(); +private: + QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId); + QStringList m_accountsLoading; }; inline QUOTIENT_API AccountRegistry Accounts {}; diff --git a/lib/connection.cpp b/lib/connection.cpp index dba18cb1..526feb26 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -41,12 +41,12 @@ # include "e2ee/qolmsession.h" # include "e2ee/qolmutils.h" -# if QT_VERSION_MAJOR >= 6 -# include -# else -# include -# endif #endif // Quotient_E2EE_ENABLED +#if QT_VERSION_MAJOR >= 6 +# include +#else +# include +#endif #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) # include @@ -368,6 +368,32 @@ public: void saveDevicesList(); void loadDevicesList(); #endif + + void saveAccessTokenToKeychain() + { + qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); + auto job = new QKeychain::WritePasswordJob(qAppName()); + job->setAutoDelete(false); + job->setKey(q->userId()); + job->setBinaryData(data->accessToken()); + job->start(); + //TODO error handling + } + + void removeAccessTokenFromKeychain() + { + qCDebug(MAIN) << "Removing access token from keychain for" << q->userId(); + auto job = new QKeychain::DeletePasswordJob(qAppName()); + job->setAutoDelete(true); + job->setKey(q->userId()); + job->start(); + + auto pickleJob = new QKeychain::DeletePasswordJob(qAppName()); + pickleJob->setAutoDelete(true); + pickleJob->setKey(q->userId() + "-Pickle"_ls); + pickleJob->start(); + //TODO error handling + } }; Connection::Connection(const QUrl& server, QObject* parent) @@ -546,6 +572,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) data->setToken(loginJob->accessToken().toLatin1()); data->setDeviceId(loginJob->deviceId()); completeSetup(loginJob->userId()); + saveAccessTokenToKeychain(); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -684,6 +711,8 @@ void Connection::logout() disconnect(d->syncLoopConnection); d->data->setToken({}); emit loggedOut(); + SettingsGroup("Accounts").remove(userId()); + d->removeAccessTokenFromKeychain(); deleteLater(); } else { // logout() somehow didn't proceed - restore the session state emit stateChanged(); -- 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. --- CMakeLists.txt | 2 +- autotests/testfilecrypto.cpp | 6 +- autotests/testolmaccount.cpp | 5 +- lib/connection.cpp | 11 ++- lib/connection.h | 5 +- lib/converters.h | 8 ++ lib/eventitem.cpp | 21 ++--- lib/eventitem.h | 9 +- lib/events/encryptedfile.cpp | 119 -------------------------- lib/events/encryptedfile.h | 63 -------------- lib/events/eventcontent.cpp | 68 +++++++-------- lib/events/eventcontent.h | 53 ++++++------ lib/events/filesourceinfo.cpp | 181 ++++++++++++++++++++++++++++++++++++++++ lib/events/filesourceinfo.h | 89 ++++++++++++++++++++ lib/events/roomavatarevent.h | 4 +- lib/events/roommessageevent.cpp | 8 +- lib/events/stickerevent.cpp | 2 +- lib/jobs/downloadfilejob.cpp | 13 +-- lib/jobs/downloadfilejob.h | 5 +- lib/mxcreply.cpp | 10 ++- lib/room.cpp | 85 +++++++++---------- lib/room.h | 3 +- quotest/quotest.cpp | 2 +- 23 files changed, 427 insertions(+), 345 deletions(-) delete mode 100644 lib/events/encryptedfile.cpp delete mode 100644 lib/events/encryptedfile.h create mode 100644 lib/events/filesourceinfo.cpp create mode 100644 lib/events/filesourceinfo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 404ba87c..635efd90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,7 +168,7 @@ list(APPEND lib_SRCS lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp lib/events/stickerevent.h lib/events/stickerevent.cpp lib/events/keyverificationevent.h lib/events/keyverificationevent.cpp - lib/events/encryptedfile.h lib/events/encryptedfile.cpp + lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp lib/jobs/requestdata.h lib/jobs/requestdata.cpp lib/jobs/basejob.h lib/jobs/basejob.cpp lib/jobs/syncjob.h lib/jobs/syncjob.cpp diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp index f9212376..b86114a4 100644 --- a/autotests/testfilecrypto.cpp +++ b/autotests/testfilecrypto.cpp @@ -3,14 +3,16 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "testfilecrypto.h" -#include "events/encryptedfile.h" + +#include "events/filesourceinfo.h" + #include using namespace Quotient; void TestFileCrypto::encryptDecryptData() { QByteArray data = "ABCDEF"; - auto [file, cipherText] = EncryptedFile::encryptFile(data); + auto [file, cipherText] = EncryptedFileMetadata::encryptFile(data); auto decrypted = file.decryptFile(cipherText); // AES CTR produces ciphertext of the same size as the original QCOMPARE(cipherText.size(), data.size()); diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index e31ff6d3..b509d12f 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -156,8 +156,7 @@ void TestOlmAccount::encryptedFile() "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" }})"); - EncryptedFile file; - JsonObjectConverter::fillFrom(doc.object(), file); + const auto file = fromJson(doc); QCOMPARE(file.v, "v2"); QCOMPARE(file.iv, "w+sE15fzSc0AAAAAAAAAAA"); diff --git a/lib/connection.cpp b/lib/connection.cpp index dba18cb1..0994d85a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1137,15 +1137,14 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, } #ifdef Quotient_E2EE_ENABLED -DownloadFileJob* Connection::downloadFile(const QUrl& url, - const EncryptedFile& file, - const QString& localFilename) +DownloadFileJob* Connection::downloadFile( + const QUrl& url, const EncryptedFileMetadata& fileMetadata, + const QString& localFilename) { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); - auto* job = - callApi(idParts.front(), idParts.back(), file, localFilename); - return job; + return callApi(idParts.front(), idParts.back(), + fileMetadata, localFilename); } #endif diff --git a/lib/connection.h b/lib/connection.h index f8744752..656e597c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -51,7 +51,7 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; class Database; -struct EncryptedFile; +struct EncryptedFileMetadata; class QOlmAccount; class QOlmInboundGroupSession; @@ -601,7 +601,8 @@ public Q_SLOTS: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file, + DownloadFileJob* downloadFile(const QUrl& url, + const EncryptedFileMetadata& file, const QString& localFilename = {}); #endif /** diff --git a/lib/converters.h b/lib/converters.h index 5e3becb8..49cb1ed9 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -16,6 +16,7 @@ #include #include +#include class QVariant; @@ -224,6 +225,13 @@ struct QUOTIENT_API JsonConverter { static QVariant load(const QJsonValue& jv); }; +template +inline QJsonValue toJson(const std::variant& v) +{ + return std::visit( + [](const auto& value) { return QJsonValue { toJson(value) }; }, v); +} + template struct JsonConverter> { static QJsonValue dump(const Omittable& from) diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 302ae053..a2e2a156 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -8,32 +8,23 @@ using namespace Quotient; -void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) +void PendingEventItem::setFileUploaded(const FileSourceInfo& uploadedFileData) { // TODO: eventually we might introduce hasFileContent to RoomEvent, // and unify the code below. if (auto* rme = getAs()) { Q_ASSERT(rme->hasFileContent()); - rme->editContent([remoteUrl](EventContent::TypedBase& ec) { - ec.fileInfo()->url = remoteUrl; + rme->editContent([&uploadedFileData](EventContent::TypedBase& ec) { + ec.fileInfo()->source = uploadedFileData; }); } if (auto* rae = getAs()) { Q_ASSERT(rae->content().fileInfo()); - rae->editContent( - [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; }); - } - setStatus(EventStatus::FileUploaded); -} - -void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) -{ - if (auto* rme = getAs()) { - Q_ASSERT(rme->hasFileContent()); - rme->editContent([encryptedFile](EventContent::TypedBase& ec) { - ec.fileInfo()->file = encryptedFile; + rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) { + fi.source = uploadedFileData; }); } + setStatus(EventStatus::FileUploaded); } // Not exactly sure why but this helps with the linker not finding diff --git a/lib/eventitem.h b/lib/eventitem.h index d8313736..5e001d88 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -3,14 +3,14 @@ #pragma once -#include "events/stateevent.h" #include "quotient_common.h" +#include "events/filesourceinfo.h" +#include "events/stateevent.h" + #include #include -#include "events/encryptedfile.h" - namespace Quotient { namespace EventStatus { @@ -115,8 +115,7 @@ public: QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } - void setFileUploaded(const QUrl& remoteUrl); - void setEncryptedFile(const EncryptedFile& encryptedFile); + void setFileUploaded(const FileSourceInfo &uploadedFileData); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp deleted file mode 100644 index 33ebb514..00000000 --- a/lib/events/encryptedfile.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "encryptedfile.h" -#include "logging.h" - -#ifdef Quotient_E2EE_ENABLED -#include -#include -#include "e2ee/qolmutils.h" -#endif - -using namespace Quotient; - -QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const -{ -#ifdef Quotient_E2EE_ENABLED - auto _key = key.k; - const auto keyBytes = QByteArray::fromBase64( - _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); - const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); - if (sha256 - != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { - qCWarning(E2EE) << "Hash verification failed for file"; - return {}; - } - { - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - - 1, - '\0'); - EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, - reinterpret_cast( - keyBytes.data()), - reinterpret_cast( - QByteArray::fromBase64(iv.toLatin1()).data())); - EVP_DecryptUpdate( - ctx, reinterpret_cast(plaintext.data()), &length, - reinterpret_cast(ciphertext.data()), - ciphertext.size()); - EVP_DecryptFinal_ex(ctx, - reinterpret_cast(plaintext.data()) - + length, - &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext.left(ciphertext.size()); - } -#else - qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " - "cannot decrypt the file"; - return ciphertext; -#endif -} - -std::pair EncryptedFile::encryptFile(const QByteArray &plainText) -{ -#ifdef Quotient_E2EE_ENABLED - QByteArray k = getRandom(32); - auto kBase64 = k.toBase64(); - QByteArray iv = getRandom(16); - JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true}; - - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); - const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); - QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); - EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); - EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); - EVP_CIPHER_CTX_free(ctx); - - auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256).toBase64(); - auto ivBase64 = iv.toBase64(); - EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; - return {file, cipherText}; -#else - return {}; -#endif -} - -void JsonObjectConverter::dumpTo(QJsonObject& jo, - const EncryptedFile& pod) -{ - addParam<>(jo, QStringLiteral("url"), pod.url); - addParam<>(jo, QStringLiteral("key"), pod.key); - addParam<>(jo, QStringLiteral("iv"), pod.iv); - addParam<>(jo, QStringLiteral("hashes"), pod.hashes); - addParam<>(jo, QStringLiteral("v"), pod.v); -} - -void JsonObjectConverter::fillFrom(const QJsonObject& jo, - EncryptedFile& pod) -{ - fromJson(jo.value("url"_ls), pod.url); - fromJson(jo.value("key"_ls), pod.key); - fromJson(jo.value("iv"_ls), pod.iv); - fromJson(jo.value("hashes"_ls), pod.hashes); - fromJson(jo.value("v"_ls), pod.v); -} - -void JsonObjectConverter::dumpTo(QJsonObject &jo, const JWK &pod) -{ - addParam<>(jo, QStringLiteral("kty"), pod.kty); - addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); - addParam<>(jo, QStringLiteral("alg"), pod.alg); - addParam<>(jo, QStringLiteral("k"), pod.k); - addParam<>(jo, QStringLiteral("ext"), pod.ext); -} - -void JsonObjectConverter::fillFrom(const QJsonObject &jo, JWK &pod) -{ - fromJson(jo.value("kty"_ls), pod.kty); - fromJson(jo.value("key_ops"_ls), pod.keyOps); - fromJson(jo.value("alg"_ls), pod.alg); - fromJson(jo.value("k"_ls), pod.k); - fromJson(jo.value("ext"_ls), pod.ext); -} diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h deleted file mode 100644 index 022ac91e..00000000 --- a/lib/events/encryptedfile.h +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "converters.h" - -namespace Quotient { -/** - * JSON Web Key object as specified in - * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes - * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. - */ -struct JWK -{ - Q_GADGET - Q_PROPERTY(QString kty MEMBER kty CONSTANT) - Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) - Q_PROPERTY(QString alg MEMBER alg CONSTANT) - Q_PROPERTY(QString k MEMBER k CONSTANT) - Q_PROPERTY(bool ext MEMBER ext CONSTANT) - -public: - QString kty; - QStringList keyOps; - QString alg; - QString k; - bool ext; -}; - -struct QUOTIENT_API EncryptedFile -{ - Q_GADGET - Q_PROPERTY(QUrl url MEMBER url CONSTANT) - Q_PROPERTY(JWK key MEMBER key CONSTANT) - Q_PROPERTY(QString iv MEMBER iv CONSTANT) - Q_PROPERTY(QHash hashes MEMBER hashes CONSTANT) - Q_PROPERTY(QString v MEMBER v CONSTANT) - -public: - QUrl url; - JWK key; - QString iv; - QHash hashes; - QString v; - - QByteArray decryptFile(const QByteArray &ciphertext) const; - static std::pair encryptFile(const QByteArray& plainText); -}; - -template <> -struct QUOTIENT_API JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const EncryptedFile& pod); - static void fillFrom(const QJsonObject& jo, EncryptedFile& pod); -}; - -template <> -struct QUOTIENT_API JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const JWK& pod); - static void fillFrom(const QJsonObject& jo, JWK& pod); -}; -} // namespace Quotient diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 6218e3b8..36b647cb 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -19,23 +19,21 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QFileInfo &fi) - : mimeType(QMimeDatabase().mimeTypeForFile(fi)) - , url(QUrl::fromLocalFile(fi.filePath())) - , payloadSize(fi.size()) - , originalName(fi.fileName()) +FileInfo::FileInfo(const QFileInfo& fi) + : source(QUrl::fromLocalFile(fi.filePath())), + mimeType(QMimeDatabase().mimeTypeForFile(fi)), + payloadSize(fi.size()), + originalName(fi.fileName()) { Q_ASSERT(fi.isFile()); } -FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, - Omittable encryptedFile, - QString originalFilename) - : mimeType(mimeType) - , url(move(u)) +FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize, + const QMimeType& mimeType, QString originalFilename) + : source(move(sourceInfo)) + , mimeType(mimeType) , payloadSize(payloadSize) , originalName(move(originalFilename)) - , file(move(encryptedFile)) { if (!isValid()) qCWarning(MESSAGES) @@ -44,28 +42,28 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, "0.7; for local resources, use FileInfo(QFileInfo) instead"; } -FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, +FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, QString originalFilename) - : originalInfoJson(infoJson) + : source(move(sourceInfo)) + , originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(move(mxcUrl)) , payloadSize(fromJson(infoJson["size"_ls])) , originalName(move(originalFilename)) - , file(move(encryptedFile)) { - if(url.isEmpty() && file.has_value()) { - url = file->url; - } if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } bool FileInfo::isValid() const { - return url.scheme() == "mxc" - && (url.authority() + url.path()).count('/') == 1; + const auto& u = url(); + return u.scheme() == "mxc" && (u.authority() + u.path()).count('/') == 1; +} + +QUrl FileInfo::url() const +{ + return getUrlFromSourceInfo(source); } QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) @@ -75,7 +73,6 @@ QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) infoJson.insert(QStringLiteral("size"), info.payloadSize); if (info.mimeType.isValid()) infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name()); - //TODO add encryptedfile return infoJson; } @@ -83,17 +80,16 @@ ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) : FileInfo(fi), imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, - QSize imageSize, Omittable encryptedFile, +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize, + const QMimeType& type, QSize imageSize, const QString& originalFilename) - : FileInfo(mxcUrl, fileSize, type, move(encryptedFile), originalFilename) + : FileInfo(move(sourceInfo), fileSize, type, originalFilename) , imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, const QString& originalFilename) - : FileInfo(mxcUrl, infoJson, move(encryptedFile), originalFilename) + : FileInfo(move(sourceInfo), infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} @@ -107,16 +103,20 @@ QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) return infoJson; } -Thumbnail::Thumbnail(const QJsonObject& infoJson, - Omittable encryptedFile) +Thumbnail::Thumbnail( + const QJsonObject& infoJson, + const Omittable& encryptedFileMetadata) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), - infoJson["thumbnail_info"_ls].toObject(), move(encryptedFile)) -{} + infoJson["thumbnail_info"_ls].toObject()) +{ + if (encryptedFileMetadata) + source = *encryptedFileMetadata; +} void Thumbnail::dumpTo(QJsonObject& infoJson) const { - if (url.isValid()) - infoJson.insert(QStringLiteral("thumbnail_url"), url.toString()); + if (url().isValid()) + fillJson(infoJson, { "thumbnail_url"_ls, "thumbnail_file"_ls }, source); if (!imageSize.isEmpty()) infoJson.insert(QStringLiteral("thumbnail_info"), toInfoJson(*this)); diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index bbd35618..23281876 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -6,14 +6,14 @@ // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). -#include "encryptedfile.h" +#include "filesourceinfo.h" #include "quotient_export.h" #include +#include #include #include #include -#include class QFileInfo; @@ -50,7 +50,7 @@ namespace EventContent { // A quick classes inheritance structure follows (the definitions are // spread across eventcontent.h and roommessageevent.h): - // UrlBasedContent : InfoT + url and thumbnail data + // UrlBasedContent : InfoT + thumbnail data // PlayableContent : + duration attribute // FileInfo // FileContent = UrlBasedContent @@ -89,34 +89,32 @@ namespace EventContent { //! //! \param fi a QFileInfo object referring to an existing file explicit FileInfo(const QFileInfo& fi); - explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, + explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, const QMimeType& mimeType = {}, - Omittable encryptedFile = none, QString originalFilename = {}); //! \brief Construct from a JSON `info` payload //! //! Make sure to pass the `info` subobject of content JSON, not the //! whole JSON content. - FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, + FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, QString originalFilename = {}); bool isValid() const; + QUrl url() const; //! \brief Extract media id from the URL //! //! This can be used, e.g., to construct a QML-facing image:// //! URI as follows: //! \code "image://provider/" + info.mediaId() \endcode - QString mediaId() const { return url.authority() + url.path(); } + QString mediaId() const { return url().authority() + url().path(); } public: + FileSourceInfo source; QJsonObject originalInfoJson; QMimeType mimeType; - QUrl url; qint64 payloadSize = 0; QString originalName; - Omittable file = none; }; QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); @@ -126,12 +124,10 @@ namespace EventContent { public: ImageInfo() = default; explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); - explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, + explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, const QMimeType& type = {}, QSize imageSize = {}, - Omittable encryptedFile = none, const QString& originalFilename = {}); - ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - Omittable encryptedFile, + ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, const QString& originalFilename = {}); public: @@ -144,12 +140,13 @@ namespace EventContent { //! //! This class saves/loads a thumbnail to/from `info` subobject of //! the JSON representation of event content; namely, `info/thumbnail_url` - //! and `info/thumbnail_info` fields are used. + //! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and + //! `info/thumbnail_info` fields are used. class QUOTIENT_API Thumbnail : public ImageInfo { public: using ImageInfo::ImageInfo; Thumbnail(const QJsonObject& infoJson, - Omittable encryptedFile = none); + const Omittable& encryptedFile = none); //! \brief Add thumbnail information to the passed `info` JSON object void dumpTo(QJsonObject& infoJson) const; @@ -169,10 +166,10 @@ namespace EventContent { //! \brief A template class for content types with a URL and additional info //! - //! Types that derive from this class template take `url` and, - //! optionally, `filename` values from the top-level JSON object and - //! the rest of information from the `info` subobject, as defined by - //! the parameter type. + //! Types that derive from this class template take `url` (or, if the file + //! is encrypted, `file`) and, optionally, `filename` values from + //! the top-level JSON object and the rest of information from the `info` + //! subobject, as defined by the parameter type. //! \tparam InfoT base info class - FileInfo or ImageInfo template class UrlBasedContent : public TypedBase, public InfoT { @@ -181,10 +178,12 @@ namespace EventContent { explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - fromJson>(json["file"]), json["filename"].toString()) , thumbnail(FileInfo::originalInfoJson) { + const auto efmJson = json.value("file"_ls).toObject(); + if (!efmJson.isEmpty()) + InfoT::source = fromJson(efmJson); // Two small hacks on originalJson to expose mediaIds to QML originalJson.insert("mediaId", InfoT::mediaId()); originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); @@ -204,11 +203,7 @@ namespace EventContent { void fillJson(QJsonObject& json) const override { - if (!InfoT::file.has_value()) { - json.insert("url", InfoT::url.toString()); - } else { - json.insert("file", Quotient::toJson(*InfoT::file)); - } + Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source); if (!InfoT::originalName.isEmpty()) json.insert("filename", InfoT::originalName); auto infoJson = toInfoJson(*this); @@ -223,7 +218,7 @@ namespace EventContent { //! //! Available fields: //! - corresponding to the top-level JSON: - //! - url + //! - source (corresponding to `url` or `file` in JSON) //! - filename (extension to the spec) //! - corresponding to the `info` subobject: //! - payloadSize (`size` in JSON) @@ -241,12 +236,12 @@ namespace EventContent { //! //! Available fields: //! - corresponding to the top-level JSON: - //! - url + //! - source (corresponding to `url` or `file` in JSON) //! - filename //! - corresponding to the `info` subobject: //! - payloadSize (`size` in JSON) //! - mimeType (`mimetype` in JSON) - //! - thumbnail.url (`thumbnail_url` in JSON) + //! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) //! - corresponding to the `info/thumbnail_info` subobject: //! - thumbnail.payloadSize //! - thumbnail.mimeType diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp new file mode 100644 index 00000000..a64c7da8 --- /dev/null +++ b/lib/events/filesourceinfo.cpp @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "filesourceinfo.h" + +#include "logging.h" + +#ifdef Quotient_E2EE_ENABLED +# include "e2ee/qolmutils.h" + +# include + +# include +#endif + +using namespace Quotient; + +QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) const +{ +#ifdef Quotient_E2EE_ENABLED + auto _key = key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + const auto sha256 = + QByteArray::fromBase64(hashes["sha256"_ls].toLatin1()); + if (sha256 + != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return {}; + } + { + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); + EVP_DecryptInit_ex( + ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast(keyBytes.data()), + reinterpret_cast( + QByteArray::fromBase64(iv.toLatin1()).data())); + EVP_DecryptUpdate( + ctx, reinterpret_cast(plaintext.data()), &length, + reinterpret_cast(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext.left(ciphertext.size()); + } +#else + qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " + "cannot decrypt the file"; + return ciphertext; +#endif +} + +std::pair EncryptedFileMetadata::encryptFile( + const QByteArray& plainText) +{ +#ifdef Quotient_E2EE_ENABLED + QByteArray k = getRandom(32); + auto kBase64 = k.toBase64(); + QByteArray iv = getRandom(16); + JWK key = { "oct"_ls, + { "encrypt"_ls, "decrypt"_ls }, + "A256CTR"_ls, + QString(k.toBase64()) + .replace(u'/', u'_') + .replace(u'+', u'-') + .left(kBase64.indexOf('=')), + true }; + + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast(k.data()), + reinterpret_cast(iv.data())); + const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); + QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); + EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), + &length, + reinterpret_cast(plainText.data()), + plainText.size()); + EVP_EncryptFinal_ex(ctx, + reinterpret_cast(cipherText.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256) + .toBase64(); + auto ivBase64 = iv.toBase64(); + EncryptedFileMetadata efm = { {}, + key, + ivBase64.left(ivBase64.indexOf('=')), + { { QStringLiteral("sha256"), + hash.left(hash.indexOf('=')) } }, + "v2"_ls }; + return { efm, cipherText }; +#else + return {}; +#endif +} + +void JsonObjectConverter::dumpTo(QJsonObject& jo, + const EncryptedFileMetadata& pod) +{ + addParam<>(jo, QStringLiteral("url"), pod.url); + addParam<>(jo, QStringLiteral("key"), pod.key); + addParam<>(jo, QStringLiteral("iv"), pod.iv); + addParam<>(jo, QStringLiteral("hashes"), pod.hashes); + addParam<>(jo, QStringLiteral("v"), pod.v); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, + EncryptedFileMetadata& pod) +{ + fromJson(jo.value("url"_ls), pod.url); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("iv"_ls), pod.iv); + fromJson(jo.value("hashes"_ls), pod.hashes); + fromJson(jo.value("v"_ls), pod.v); +} + +void JsonObjectConverter::dumpTo(QJsonObject& jo, const JWK& pod) +{ + addParam<>(jo, QStringLiteral("kty"), pod.kty); + addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); + addParam<>(jo, QStringLiteral("alg"), pod.alg); + addParam<>(jo, QStringLiteral("k"), pod.k); + addParam<>(jo, QStringLiteral("ext"), pod.ext); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, JWK& pod) +{ + fromJson(jo.value("kty"_ls), pod.kty); + fromJson(jo.value("key_ops"_ls), pod.keyOps); + fromJson(jo.value("alg"_ls), pod.alg); + fromJson(jo.value("k"_ls), pod.k); + fromJson(jo.value("ext"_ls), pod.ext); +} + +template +struct Overloads : FunctorTs... { + using FunctorTs::operator()...; +}; + +template +Overloads(FunctorTs&&...) -> Overloads; + +QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi) +{ + return std::visit(Overloads { [](const QUrl& url) { return url; }, + [](const EncryptedFileMetadata& efm) { + return efm.url; + } }, + fsi); +} + +void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl) +{ + std::visit(Overloads { [&newUrl](QUrl& url) { url = newUrl; }, + [&newUrl](EncryptedFileMetadata& efm) { + efm.url = newUrl; + } }, + fsi); +} + +void Quotient::fillJson(QJsonObject& jo, + const std::array& jsonKeys, + const FileSourceInfo& fsi) +{ + // NB: Keeping variant_size_v out of the function signature for readability. + // NB2: Can't use jsonKeys directly inside static_assert as its value is + // unknown so the compiler cannot ensure size() is constexpr (go figure...) + static_assert( + std::variant_size_v == decltype(jsonKeys) {}.size()); + jo.insert(jsonKeys[fsi.index()], toJson(fsi)); +} diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h new file mode 100644 index 00000000..885601be --- /dev/null +++ b/lib/events/filesourceinfo.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +#include + +namespace Quotient { +/** + * JSON Web Key object as specified in + * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes + * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. + */ +struct JWK +{ + Q_GADGET + Q_PROPERTY(QString kty MEMBER kty CONSTANT) + Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) + Q_PROPERTY(QString alg MEMBER alg CONSTANT) + Q_PROPERTY(QString k MEMBER k CONSTANT) + Q_PROPERTY(bool ext MEMBER ext CONSTANT) + +public: + QString kty; + QStringList keyOps; + QString alg; + QString k; + bool ext; +}; + +struct QUOTIENT_API EncryptedFileMetadata { + Q_GADGET + Q_PROPERTY(QUrl url MEMBER url CONSTANT) + Q_PROPERTY(JWK key MEMBER key CONSTANT) + Q_PROPERTY(QString iv MEMBER iv CONSTANT) + Q_PROPERTY(QHash hashes MEMBER hashes CONSTANT) + Q_PROPERTY(QString v MEMBER v CONSTANT) + +public: + QUrl url; + JWK key; + QString iv; + QHash hashes; + QString v; + + static std::pair encryptFile( + const QByteArray& plainText); + QByteArray decryptFile(const QByteArray& ciphertext) const; +}; + +template <> +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod); + static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod); +}; + +template <> +struct QUOTIENT_API JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const JWK& pod); + static void fillFrom(const QJsonObject& jo, JWK& pod); +}; + +using FileSourceInfo = std::variant; + +QUOTIENT_API QUrl getUrlFromSourceInfo(const FileSourceInfo& fsi); + +QUOTIENT_API void setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl); + +// The way FileSourceInfo is stored in JSON requires an extra parameter so +// the original template is not applicable +template <> +void fillJson(QJsonObject&, const FileSourceInfo&) = delete; + +//! \brief Export FileSourceInfo to a JSON object +//! +//! Depending on what is stored inside FileSourceInfo, this function will insert +//! - a key-to-string pair where key is taken from jsonKeys[0] and the string +//! is the URL, if FileSourceInfo stores a QUrl; +//! - a key-to-object mapping where key is taken from jsonKeys[1] and the object +//! is the result of converting EncryptedFileMetadata to JSON, +//! if FileSourceInfo stores EncryptedFileMetadata +QUOTIENT_API void fillJson(QJsonObject& jo, + const std::array& jsonKeys, + const FileSourceInfo& fsi); + +} // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index c54b5801..af291696 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -26,10 +26,10 @@ public: const QSize& imageSize = {}, const QString& originalFilename = {}) : RoomAvatarEvent(EventContent::ImageContent { - mxcUrl, fileSize, mimeType, imageSize, none, originalFilename }) + mxcUrl, fileSize, mimeType, imageSize, originalFilename }) {} - QUrl url() const { return content().url; } + QUrl url() const { return content().url(); } }; REGISTER_EVENT_TYPE(RoomAvatarEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index d9d3fbe0..2a6ae93c 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -148,21 +148,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, - QImageReader(filePath).size(), none, + QImageReader(filePath).size(), file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, - QMediaResource(localUrl).resolution(), none, + QMediaResource(localUrl).resolution(), file.fileName()); if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, file.size(), mimeType, none, + return new AudioContent(localUrl, file.size(), mimeType, file.fileName()); } - return new FileContent(localUrl, file.size(), mimeType, none, file.fileName()); + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp index 628fd154..6d318f0e 100644 --- a/lib/events/stickerevent.cpp +++ b/lib/events/stickerevent.cpp @@ -22,5 +22,5 @@ const EventContent::ImageContent &StickerEvent::image() const QUrl StickerEvent::url() const { - return m_imageContent.url; + return m_imageContent.url(); } diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index d00fc5f4..85c235c7 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -8,8 +8,9 @@ #include #ifdef Quotient_E2EE_ENABLED -# include -# include "events/encryptedfile.h" +# include "events/filesourceinfo.h" + +# include #endif using namespace Quotient; @@ -26,7 +27,7 @@ public: QScopedPointer tempFile; #ifdef Quotient_E2EE_ENABLED - Omittable encryptedFile; + Omittable encryptedFile; #endif }; @@ -49,7 +50,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, #ifdef Quotient_E2EE_ENABLED DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, - const EncryptedFile& file, + const EncryptedFileMetadata& file, const QString& localFilename) : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? makeImpl() @@ -126,7 +127,7 @@ BaseJob::Status DownloadFileJob::prepareResult() d->tempFile->seek(0); QByteArray encrypted = d->tempFile->readAll(); - EncryptedFile file = *d->encryptedFile; + EncryptedFileMetadata file = *d->encryptedFile; const auto decrypted = file.decryptFile(encrypted); d->targetFile->write(decrypted); d->tempFile->remove(); @@ -151,7 +152,7 @@ BaseJob::Status DownloadFileJob::prepareResult() d->tempFile->seek(0); const auto encrypted = d->tempFile->readAll(); - EncryptedFile file = *d->encryptedFile; + EncryptedFileMetadata file = *d->encryptedFile; const auto decrypted = file.decryptFile(encrypted); d->tempFile->write(decrypted); } else { diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index ffa3d055..cbbfd244 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -4,7 +4,8 @@ #pragma once #include "csapi/content-repo.h" -#include "events/encryptedfile.h" + +#include "events/filesourceinfo.h" namespace Quotient { class QUOTIENT_API DownloadFileJob : public GetContentJob { @@ -16,7 +17,7 @@ public: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {}); + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {}); #endif QString targetFileName() const; diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 319d514a..b7993ad5 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -8,7 +8,7 @@ #include "room.h" #ifdef Quotient_E2EE_ENABLED -#include "events/encryptedfile.h" +#include "events/filesourceinfo.h" #endif using namespace Quotient; @@ -20,7 +20,7 @@ public: : m_reply(r) {} QNetworkReply* m_reply; - Omittable m_encryptedFile; + Omittable m_encryptedFile; QIODevice* m_device = nullptr; }; @@ -47,7 +47,7 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) if(!d->m_encryptedFile.has_value()) { d->m_device = d->m_reply; } else { - EncryptedFile file = *d->m_encryptedFile; + EncryptedFileMetadata file = *d->m_encryptedFile; auto buffer = new QBuffer(this); buffer->setData(file.decryptFile(d->m_reply->readAll())); buffer->open(ReadOnly); @@ -64,7 +64,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) auto eventIt = room->findInTimeline(eventId); if(eventIt != room->historyEdge()) { auto event = eventIt->viewAs(); - d->m_encryptedFile = event->content()->fileInfo()->file; + if (auto* efm = std::get_if( + &event->content()->fileInfo()->source)) + d->m_encryptedFile = *efm; } #endif } 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, diff --git a/lib/room.h b/lib/room.h index c3bdc4a0..0636c4bb 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,8 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile); + void fileTransferCompleted(QString id, QUrl localFile, + FileSourceInfo fileMetadata); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 1eed865f..6bcd71cd 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -516,7 +516,7 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, && e.hasFileContent() && e.content()->fileInfo()->originalName == fileName && testDownload(targetRoom->connection()->makeMediaUrl( - e.content()->fileInfo()->url))); + e.content()->fileInfo()->url()))); }, [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); }); -- 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/connection.cpp | 57 ++++++++++++++++++++++++++++-------------------------- lib/connection.h | 17 ++++++++++------ lib/room.cpp | 14 ++++++-------- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0994d85a..8fd2d6cf 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1322,18 +1322,11 @@ Connection::sendToDevices(const QString& eventType, { QHash> json; json.reserve(int(eventsMap.size())); - std::for_each(eventsMap.begin(), eventsMap.end(), - [&json](const auto& userTodevicesToEvents) { - auto& jsonUser = json[userTodevicesToEvents.first]; - const auto& devicesToEvents = userTodevicesToEvents.second; - std::for_each(devicesToEvents.begin(), - devicesToEvents.end(), - [&jsonUser](const auto& deviceToEvents) { - jsonUser.insert( - deviceToEvents.first, - deviceToEvents.second->contentJson()); - }); - }); + for (const auto& [userId, devicesToEvents] : eventsMap) { + auto& jsonUser = json[userId]; + for (const auto& [deviceId, event] : devicesToEvents) + jsonUser.insert(deviceId, event->contentJson()); + } return callApi(BackgroundRequest, eventType, generateTxnId(), json); } @@ -2218,20 +2211,23 @@ QStringList Connection::devicesForUser(const QString& userId) const return d->deviceKeys[userId].keys(); } -QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const +QString Connection::curveKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[user][device].keys["curve25519:" % device]; + return d->deviceKeys[userId][device].keys["curve25519:" % device]; } -QString Connection::edKeyForUserDevice(const QString& user, const QString& device) const +QString Connection::edKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[user][device].keys["ed25519:" % device]; + return d->deviceKeys[userId][device].keys["ed25519:" % device]; } -bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) +bool Connection::isKnownCurveKey(const QString& userId, + const QString& curveKey) const { auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey")); - query.bindValue(":matrixId", user); + query.bindValue(":matrixId", userId); query.bindValue(":curveKey", curveKey); database()->execute(query); return query.next(); @@ -2243,25 +2239,32 @@ bool Connection::hasOlmSession(const QString& user, const QString& deviceId) con return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } -QPair Connection::olmEncryptMessage(const QString& user, const QString& device, const QByteArray& message) +std::pair Connection::olmEncryptMessage( + const QString& userId, const QString& device, const QByteArray& message) const { - const auto& curveKey = curveKeyForUserDevice(user, device); + const auto& curveKey = curveKeyForUserDevice(userId, device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); - auto result = d->olmSessions[curveKey][0]->encrypt(message); - auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); - if (pickle) { - database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle); + const auto result = d->olmSessions[curveKey][0]->encrypt(message); + if (const auto pickle = + d->olmSessions[curveKey][0]->pickle(picklingMode())) { + database()->updateOlmSession(curveKey, + d->olmSessions[curveKey][0]->sessionId(), + *pickle); } else { qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } return { type, result.toCiphertext() }; } -void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) +void Connection::createOlmSession(const QString& theirIdentityKey, + const QString& theirOneTimeKey) const { - auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); + auto session = QOlmSession::createOutboundSession(olmAccount(), + theirIdentityKey, + theirOneTimeKey); if (!session) { - qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << session.error(); + qCWarning(E2EE) << "Failed to create olm session for " + << theirIdentityKey << session.error(); return; } d->saveSession(**session, theirIdentityKey); diff --git a/lib/connection.h b/lib/connection.h index 656e597c..72383abb 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -330,8 +330,11 @@ public: //This assumes that an olm session with (user, device) exists - QPair olmEncryptMessage(const QString& userId, const QString& device, const QByteArray& message); - void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); + std::pair olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const; + void createOlmSession(const QString& theirIdentityKey, + const QString& theirOneTimeKey) const; #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; @@ -695,10 +698,12 @@ public Q_SLOTS: PicklingMode picklingMode() const; QJsonObject decryptNotification(const QJsonObject ¬ification); - QStringList devicesForUser(const QString& user) const; - QString curveKeyForUserDevice(const QString &user, const QString& device) const; - QString edKeyForUserDevice(const QString& user, const QString& device) const; - bool isKnownCurveKey(const QString& user, const QString& curveKey); + QStringList devicesForUser(const QString& userId) const; + QString curveKeyForUserDevice(const QString& userId, + const QString& device) const; + QString edKeyForUserDevice(const QString& userId, + const QString& device) const; + bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; #endif Q_SIGNALS: /// \brief Initial server resolution has failed 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. --- autotests/testfilecrypto.cpp | 4 ++-- lib/events/filesourceinfo.cpp | 11 ++++++----- lib/events/filesourceinfo.h | 9 +++++---- lib/jobs/downloadfilejob.cpp | 4 ++-- lib/mxcreply.cpp | 4 ++-- lib/room.cpp | 3 +-- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp index b86114a4..29521060 100644 --- a/autotests/testfilecrypto.cpp +++ b/autotests/testfilecrypto.cpp @@ -12,8 +12,8 @@ using namespace Quotient; void TestFileCrypto::encryptDecryptData() { QByteArray data = "ABCDEF"; - auto [file, cipherText] = EncryptedFileMetadata::encryptFile(data); - auto decrypted = file.decryptFile(cipherText); + auto [file, cipherText] = encryptFile(data); + auto decrypted = decryptFile(cipherText, file); // AES CTR produces ciphertext of the same size as the original QCOMPARE(cipherText.size(), data.size()); QCOMPARE(decrypted.size(), data.size()); diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index a64c7da8..43e8e44c 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -16,14 +16,15 @@ using namespace Quotient; -QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) const +QByteArray Quotient::decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata) { #ifdef Quotient_E2EE_ENABLED - auto _key = key.k; + auto _key = metadata.key.k; const auto keyBytes = QByteArray::fromBase64( _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); const auto sha256 = - QByteArray::fromBase64(hashes["sha256"_ls].toLatin1()); + QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()); if (sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { qCWarning(E2EE) << "Hash verification failed for file"; @@ -37,7 +38,7 @@ QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) cons ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(keyBytes.data()), reinterpret_cast( - QByteArray::fromBase64(iv.toLatin1()).data())); + QByteArray::fromBase64(metadata.iv.toLatin1()).data())); EVP_DecryptUpdate( ctx, reinterpret_cast(plaintext.data()), &length, reinterpret_cast(ciphertext.data()), @@ -56,7 +57,7 @@ QByteArray EncryptedFileMetadata::decryptFile(const QByteArray& ciphertext) cons #endif } -std::pair EncryptedFileMetadata::encryptFile( +std::pair Quotient::encryptFile( const QByteArray& plainText) { #ifdef Quotient_E2EE_ENABLED diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h index 885601be..8f7e3cbe 100644 --- a/lib/events/filesourceinfo.h +++ b/lib/events/filesourceinfo.h @@ -45,12 +45,13 @@ public: QString iv; QHash hashes; QString v; - - static std::pair encryptFile( - const QByteArray& plainText); - QByteArray decryptFile(const QByteArray& ciphertext) const; }; +QUOTIENT_API std::pair encryptFile( + const QByteArray& plainText); +QUOTIENT_API QByteArray decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata); + template <> struct QUOTIENT_API JsonObjectConverter { static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod); diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 85c235c7..032b24f2 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -128,7 +128,7 @@ BaseJob::Status DownloadFileJob::prepareResult() QByteArray encrypted = d->tempFile->readAll(); EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = file.decryptFile(encrypted); + const auto decrypted = decryptFile(encrypted, file); d->targetFile->write(decrypted); d->tempFile->remove(); } else { @@ -153,7 +153,7 @@ BaseJob::Status DownloadFileJob::prepareResult() const auto encrypted = d->tempFile->readAll(); EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = file.decryptFile(encrypted); + const auto decrypted = decryptFile(encrypted, file); d->tempFile->write(decrypted); } else { #endif diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index b7993ad5..4174cfd8 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -47,9 +47,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) if(!d->m_encryptedFile.has_value()) { d->m_device = d->m_reply; } else { - EncryptedFileMetadata file = *d->m_encryptedFile; auto buffer = new QBuffer(this); - buffer->setData(file.decryptFile(d->m_reply->readAll())); + buffer->setData( + decryptFile(d->m_reply->readAll(), *d->m_encryptedFile)); buffer->open(ReadOnly); d->m_device = buffer; } 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 0e1f49a4ab8e6903709f387c154c2bf131a1370c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 26 May 2022 12:56:56 +0200 Subject: DownloadFileJob: refactor file decryption --- lib/jobs/downloadfilejob.cpp | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 032b24f2..99c2bf9b 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -27,7 +27,7 @@ public: QScopedPointer tempFile; #ifdef Quotient_E2EE_ENABLED - Omittable encryptedFile; + Omittable encryptedFileMetadata; #endif }; @@ -57,7 +57,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, : makeImpl(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); - d->encryptedFile = file; + d->encryptedFileMetadata = file; } #endif QString DownloadFileJob::targetFileName() const @@ -119,17 +119,21 @@ void DownloadFileJob::beforeAbandon() d->tempFile->remove(); } +void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata, + QFile& targetFile) +{ + sourceFile.seek(0); + const auto encrypted = sourceFile.readAll(); // TODO: stream decryption + const auto decrypted = decryptFile(encrypted, metadata); + targetFile.write(decrypted); +} + BaseJob::Status DownloadFileJob::prepareResult() { if (d->targetFile) { #ifdef Quotient_E2EE_ENABLED - if (d->encryptedFile.has_value()) { - d->tempFile->seek(0); - QByteArray encrypted = d->tempFile->readAll(); - - EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = decryptFile(encrypted, file); - d->targetFile->write(decrypted); + if (d->encryptedFileMetadata.has_value()) { + decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile); d->tempFile->remove(); } else { #endif @@ -148,13 +152,20 @@ BaseJob::Status DownloadFileJob::prepareResult() #endif } else { #ifdef Quotient_E2EE_ENABLED - if (d->encryptedFile.has_value()) { - d->tempFile->seek(0); - const auto encrypted = d->tempFile->readAll(); - - EncryptedFileMetadata file = *d->encryptedFile; - const auto decrypted = decryptFile(encrypted, file); - d->tempFile->write(decrypted); + if (d->encryptedFileMetadata.has_value()) { + QTemporaryFile tempTempFile; // Assuming it to be next to tempFile + decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); + d->tempFile->close(); + if (!d->tempFile->remove()) { + qCWarning(JOBS) + << "Failed to remove the decrypted file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!tempTempFile.rename(d->tempFile->fileName())) { + qCWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() + << "to" << d->tempFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } } else { #endif d->tempFile->close(); -- 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/connection.h | 2 +- lib/events/eventcontent.cpp | 9 ++++---- lib/events/eventcontent.h | 6 +++--- lib/events/filesourceinfo.cpp | 49 ++++++++++++++++++++----------------------- lib/jobs/downloadfilejob.cpp | 10 ++++----- lib/room.cpp | 2 +- 6 files changed, 37 insertions(+), 41 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 72383abb..43e285c1 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -605,7 +605,7 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED DownloadFileJob* downloadFile(const QUrl& url, - const EncryptedFileMetadata& file, + const EncryptedFileMetadata& fileMetadata, const QString& localFilename = {}); #endif /** diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 36b647cb..8db3b7e3 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -103,14 +103,13 @@ QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) return infoJson; } -Thumbnail::Thumbnail( - const QJsonObject& infoJson, - const Omittable& encryptedFileMetadata) +Thumbnail::Thumbnail(const QJsonObject& infoJson, + const Omittable& efm) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), infoJson["thumbnail_info"_ls].toObject()) { - if (encryptedFileMetadata) - source = *encryptedFileMetadata; + if (efm) + source = *efm; } void Thumbnail::dumpTo(QJsonObject& infoJson) const diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 23281876..ea240122 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -146,7 +146,7 @@ namespace EventContent { public: using ImageInfo::ImageInfo; Thumbnail(const QJsonObject& infoJson, - const Omittable& encryptedFile = none); + const Omittable& efm = none); //! \brief Add thumbnail information to the passed `info` JSON object void dumpTo(QJsonObject& infoJson) const; @@ -181,8 +181,8 @@ namespace EventContent { json["filename"].toString()) , thumbnail(FileInfo::originalInfoJson) { - const auto efmJson = json.value("file"_ls).toObject(); - if (!efmJson.isEmpty()) + if (const auto efmJson = json.value("file"_ls).toObject(); + !efmJson.isEmpty()) InfoT::source = fromJson(efmJson); // Two small hacks on originalJson to expose mediaIds to QML originalJson.insert("mediaId", InfoT::mediaId()); diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index 43e8e44c..11f93d80 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -20,36 +20,33 @@ QByteArray Quotient::decryptFile(const QByteArray& ciphertext, const EncryptedFileMetadata& metadata) { #ifdef Quotient_E2EE_ENABLED - auto _key = metadata.key.k; - const auto keyBytes = QByteArray::fromBase64( - _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); - const auto sha256 = - QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()); - if (sha256 + if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()) != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { qCWarning(E2EE) << "Hash verification failed for file"; return {}; } - { - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); - EVP_DecryptInit_ex( - ctx, EVP_aes_256_ctr(), nullptr, - reinterpret_cast(keyBytes.data()), - reinterpret_cast( - QByteArray::fromBase64(metadata.iv.toLatin1()).data())); - EVP_DecryptUpdate( - ctx, reinterpret_cast(plaintext.data()), &length, - reinterpret_cast(ciphertext.data()), - ciphertext.size()); - EVP_DecryptFinal_ex(ctx, - reinterpret_cast(plaintext.data()) - + length, - &length); - EVP_CIPHER_CTX_free(ctx); - return plaintext.left(ciphertext.size()); - } + + auto _key = metadata.key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); + EVP_DecryptInit_ex( + ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast(keyBytes.data()), + reinterpret_cast( + QByteArray::fromBase64(metadata.iv.toLatin1()).data())); + EVP_DecryptUpdate(ctx, reinterpret_cast(plaintext.data()), + &length, + reinterpret_cast(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext.left(ciphertext.size()); #else qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " "cannot decrypt the file"; diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 99c2bf9b..759d52c9 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -139,11 +139,11 @@ BaseJob::Status DownloadFileJob::prepareResult() #endif d->targetFile->close(); if (!d->targetFile->remove()) { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; + qWarning(JOBS) << "Failed to remove the target file placeholder"; return { FileError, "Couldn't finalise the download" }; } if (!d->tempFile->rename(d->targetFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() << "to" << d->targetFile->fileName(); return { FileError, "Couldn't finalise the download" }; } @@ -157,12 +157,12 @@ BaseJob::Status DownloadFileJob::prepareResult() decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); d->tempFile->close(); if (!d->tempFile->remove()) { - qCWarning(JOBS) + qWarning(JOBS) << "Failed to remove the decrypted file placeholder"; return { FileError, "Couldn't finalise the download" }; } if (!tempTempFile.rename(d->tempFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() + qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() << "to" << d->tempFile->fileName(); return { FileError, "Couldn't finalise the download" }; } @@ -173,6 +173,6 @@ BaseJob::Status DownloadFileJob::prepareResult() } #endif } - qCDebug(JOBS) << "Saved a file as" << targetFileName(); + qDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } 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/connection.cpp | 14 ++-- lib/connection.h | 8 +- lib/database.cpp | 21 ++++-- lib/database.h | 42 ++++++++--- lib/e2ee/qolmoutboundsession.cpp | 4 +- lib/e2ee/qolmoutboundsession.h | 4 +- lib/room.cpp | 156 +++++++++++++++++++++++---------------- 7 files changed, 151 insertions(+), 98 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 8fd2d6cf..1193eb75 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2187,7 +2187,7 @@ QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) return decrypted ? decrypted->fullJson() : QJsonObject(); } -Database* Connection::database() +Database* Connection::database() const { return d->database; } @@ -2271,14 +2271,18 @@ void Connection::createOlmSession(const QString& theirIdentityKey, d->olmSessions[theirIdentityKey].push_back(std::move(*session)); } -QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession( + const QString& roomId) const { - return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode); + return d->database->loadCurrentOutboundMegolmSession(roomId, + d->picklingMode); } -void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data) +void Connection::saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const { - d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data); + d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode, + session); } #endif diff --git a/lib/connection.h b/lib/connection.h index 43e285c1..a2824744 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -318,16 +318,16 @@ public: bool isLoggedIn() const; #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; - Database* database(); + Database* database() const; UnorderedMap loadRoomMegolmSessions( const Room* room); void saveMegolmSession(const Room* room, const QOlmInboundGroupSession& session); bool hasOlmSession(const QString& user, const QString& deviceId) const; - QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); - void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); - + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( + const QString& roomId) const; + void saveCurrentOutboundMegolmSession(const QString& roomId, const QOlmOutboundGroupSession &session) const; //This assumes that an olm session with (user, device) exists std::pair olmEncryptMessage( diff --git a/lib/database.cpp b/lib/database.cpp index 0119b35c..193ff54e 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -307,20 +307,22 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi commit(); } -void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) +void Database::saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session) { - const auto pickle = session->pickle(picklingMode); + const auto pickle = session.pickle(picklingMode); if (pickle) { auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); deleteQuery.bindValue(":roomId", roomId); - deleteQuery.bindValue(":sessionId", session->sessionId()); + deleteQuery.bindValue(":sessionId", session.sessionId()); auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); insertQuery.bindValue(":roomId", roomId); - insertQuery.bindValue(":sessionId", session->sessionId()); + insertQuery.bindValue(":sessionId", session.sessionId()); insertQuery.bindValue(":pickle", pickle.value()); - insertQuery.bindValue(":creationTime", session->creationTime()); - insertQuery.bindValue(":messageCount", session->messageCount()); + insertQuery.bindValue(":creationTime", session.creationTime()); + insertQuery.bindValue(":messageCount", session.messageCount()); transaction(); execute(deleteQuery); @@ -362,7 +364,9 @@ void Database::setDevicesReceivedKey(const QString& roomId, const QVector Database::devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId) +QMultiHash Database::devicesWithoutKey( + const QString& roomId, QMultiHash devices, + const QString& sessionId) { auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); query.bindValue(":roomId", roomId); @@ -371,7 +375,8 @@ QHash Database::devicesWithoutKey(const QString& roomId, Q execute(query); commit(); while (query.next()) { - devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString()); + devices.remove(query.value("userId").toString(), + query.value("deviceId").toString()); } return devices; } diff --git a/lib/database.h b/lib/database.h index 45348c8d..4091d61b 100644 --- a/lib/database.h +++ b/lib/database.h @@ -32,22 +32,40 @@ public: QByteArray accountPickle(); void setAccountPickle(const QByteArray &pickle); void clear(); - void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle, const QDateTime& timestamp); - UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); - UnorderedMap loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); - void saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId); - void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); - std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); + void saveOlmSession(const QString& senderKey, const QString& sessionId, + const QByteArray& pickle, const QDateTime& timestamp); + UnorderedMap> loadOlmSessions( + const PicklingMode& picklingMode); + UnorderedMap loadMegolmSessions( + const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& roomId, const QString& sessionId, + const QByteArray& pickle, const QString& senderId, + const QString& olmSessionId); + void addGroupSessionIndexRecord(const QString& roomId, + const QString& sessionId, uint32_t index, + const QString& eventId, qint64 ts); + std::pair groupSessionIndexRecord(const QString& roomId, + const QString& sessionId, + qint64 index); void clearRoomData(const QString& roomId); - void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); - QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); - void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); - void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); + void setOlmSessionLastReceived(const QString& sessionId, + const QDateTime& timestamp); + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode); + void saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session); + void updateOlmSession(const QString& senderKey, const QString& sessionId, + const QByteArray& pickle); // Returns a map UserId -> [DeviceId] that have not received key yet - QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); + QMultiHash devicesWithoutKey(const QString& roomId, QMultiHash devices, + const QString& sessionId); // 'devices' contains tuples {userId, deviceId, curveKey} - void setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index); + void setDevicesReceivedKey( + const QString& roomId, + const QVector>& devices, + const QString& sessionId, int index); private: void migrateTo1(); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 76188d08..a2eff2c8 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -44,7 +44,7 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() return std::make_unique(olmOutboundGroupSession); } -QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); QByteArray key = toKey(mode); @@ -79,7 +79,7 @@ QOlmExpected QOlmOutboundGroupSession::unpickle(con return std::make_unique(olmOutboundGroupSession); } -QOlmExpected QOlmOutboundGroupSession::encrypt(const QString &plaintext) +QOlmExpected QOlmOutboundGroupSession::encrypt(const QString &plaintext) const { QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index c20613d3..9a82d22a 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -21,14 +21,14 @@ public: //! Throw OlmError on errors static QOlmOutboundGroupSessionPtr create(); //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. - QOlmExpected pickle(const PicklingMode &mode); + QOlmExpected pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( const QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. - QOlmExpected encrypt(const QString& plaintext); + QOlmExpected encrypt(const QString& plaintext) const; //! Get the current message index for this session. //! 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. --- autotests/testolmaccount.cpp | 80 +++++++-------- lib/connection.cpp | 227 ++++++++++++++++++++++++++++++++++--------- lib/connection.h | 34 +++---- lib/csapi/keys.h | 4 +- lib/e2ee/qolmsession.cpp | 5 +- lib/e2ee/qolmsession.h | 2 +- lib/events/roomkeyevent.h | 4 +- lib/room.cpp | 113 +-------------------- 8 files changed, 249 insertions(+), 220 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index b509d12f..3fb8ac24 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -378,47 +378,47 @@ void TestOlmAccount::claimKeys() // Alice retrieves bob's keys & claims one signed one-time key. QHash deviceKeys; deviceKeys[bob->userId()] = QStringList(); - auto job = alice->callApi(deviceKeys); - connect(job, &BaseJob::result, this, [bob, alice, job, this] { - const auto& bobDevices = job->deviceKeys().value(bob->userId()); - QVERIFY(!bobDevices.empty()); - - // Retrieve the identity key for the current device. - const auto& bobEd25519 = - bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()]; - - const auto currentDevice = bobDevices[bob->deviceId()]; - - // Verify signature. - QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), - bob->userId())); - - QHash> oneTimeKeys; - oneTimeKeys[bob->userId()] = QHash(); - oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; - - auto job = alice->callApi(oneTimeKeys); - connect(job, &BaseJob::result, this, [bob, bobEd25519, job] { - const auto userId = bob->userId(); - const auto deviceId = bob->deviceId(); - - // The device exists. - QCOMPARE(job->oneTimeKeys().size(), 1); - QCOMPARE(job->oneTimeKeys().value(userId).size(), 1); - - // The key is the one bob sent. - const auto& oneTimeKey = - job->oneTimeKeys().value(userId).value(deviceId); - QVERIFY(oneTimeKey.canConvert()); - - const auto varMap = oneTimeKey.toMap(); - QVERIFY(std::any_of(varMap.constKeyValueBegin(), - varMap.constKeyValueEnd(), [](const auto& kv) { - return kv.first.startsWith( - SignedCurve25519Key); - })); - }); + auto queryKeysJob = alice->callApi(deviceKeys); + QSignalSpy requestSpy2(queryKeysJob, &BaseJob::result); + QVERIFY(requestSpy2.wait(10000)); + + const auto& bobDevices = queryKeysJob->deviceKeys().value(bob->userId()); + QVERIFY(!bobDevices.empty()); + + const auto currentDevice = bobDevices[bob->deviceId()]; + + // Verify signature. + QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), + bob->userId())); + // Retrieve the identity key for the current device. + const auto& bobEd25519 = + bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()]; + + QHash> oneTimeKeys; + oneTimeKeys[bob->userId()] = QHash(); + oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; + + auto claimKeysJob = alice->callApi(oneTimeKeys); + connect(claimKeysJob, &BaseJob::result, this, [bob, bobEd25519, claimKeysJob] { + const auto userId = bob->userId(); + const auto deviceId = bob->deviceId(); + + // The device exists. + QCOMPARE(claimKeysJob->oneTimeKeys().size(), 1); + QCOMPARE(claimKeysJob->oneTimeKeys().value(userId).size(), 1); + + // The key is the one bob sent. + const auto& oneTimeKeys = + claimKeysJob->oneTimeKeys().value(userId).value(deviceId); + for (auto it = oneTimeKeys.begin(); it != oneTimeKeys.end(); ++it) { + if (it.key().startsWith(SignedCurve25519Key) + && it.value().isObject()) + return; + } + QFAIL("The claimed one time key is not in /claim response"); }); + QSignalSpy completionSpy(claimKeysJob, &BaseJob::result); + QVERIFY(completionSpy.wait(10000)); } void TestOlmAccount::claimMultipleKeys() diff --git a/lib/connection.cpp b/lib/connection.cpp index 1193eb75..ab4a7dea 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -39,6 +39,7 @@ # include "e2ee/qolmaccount.h" # include "e2ee/qolminboundsession.h" # include "e2ee/qolmsession.h" +# include "e2ee/qolmutility.h" # include "e2ee/qolmutils.h" # if QT_VERSION_MAJOR >= 6 @@ -62,7 +63,6 @@ #include #include - using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() @@ -210,11 +210,11 @@ public: #ifdef Quotient_E2EE_ENABLED void loadSessions() { - olmSessions = q->database()->loadOlmSessions(q->picklingMode()); + olmSessions = q->database()->loadOlmSessions(picklingMode); } - void saveSession(QOlmSession& session, const QString& senderKey) + void saveSession(const QOlmSession& session, const QString& senderKey) const { - if (auto pickleResult = session.pickle(q->picklingMode())) + if (auto pickleResult = session.pickle(picklingMode)) q->database()->saveOlmSession(senderKey, session.sessionId(), *pickleResult, QDateTime::currentDateTime()); @@ -364,9 +364,27 @@ public: #endif // Quotient_E2EE_ENABLED } #ifdef Quotient_E2EE_ENABLED + bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; + void loadOutdatedUserDevices(); void saveDevicesList(); void loadDevicesList(); + + // This function assumes that an olm session with (user, device) exists + std::pair olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const; + bool createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const QJsonObject& oneTimeKeyObject); + QString curveKeyForUserDevice(const QString& userId, + const QString& device) const; + QString edKeyForUserDevice(const QString& userId, + const QString& device) const; + std::unique_ptr makeEventForSessionKey( + const QString& roomId, const QString& targetUserId, + const QString& targetDeviceId, const QByteArray& sessionId, + const QByteArray& sessionKey) const; #endif }; @@ -935,20 +953,23 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED if (!toDeviceEvents.empty()) { - qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() + << "to-device events"; visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); + qCDebug(E2EE) << "Unsupported algorithm" << event.id() + << "for event" << event.algorithm(); return; } - if (q->isKnownCurveKey(event.senderId(), event.senderKey())) { + if (isKnownCurveKey(event.senderId(), event.senderKey())) { handleEncryptedToDeviceEvent(event); return; } trackedUsers += event.senderId(); outdatedUsers += event.senderId(); encryptionUpdateRequired = true; - pendingEncryptedEvents.push_back(std::make_unique(event.fullJson())); + pendingEncryptedEvents.push_back( + makeEvent(event.fullJson())); }); } #endif @@ -1316,9 +1337,8 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) return forgetJob; } -SendToDeviceJob* -Connection::sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) +SendToDeviceJob* Connection::sendToDevices( + const QString& eventType, const UsersToDevicesToEvents& eventsMap) { QHash> json; json.reserve(int(eventsMap.size())); @@ -2063,7 +2083,7 @@ void Connection::Private::loadOutdatedUserDevices() saveDevicesList(); for(size_t i = 0; i < pendingEncryptedEvents.size();) { - if (q->isKnownCurveKey( + if (isKnownCurveKey( pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), pendingEncryptedEvents[i]->contentPart("sender_key"_ls))) { handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]); @@ -2193,13 +2213,13 @@ Database* Connection::database() const } UnorderedMap -Connection::loadRoomMegolmSessions(const Room* room) +Connection::loadRoomMegolmSessions(const Room* room) const { return database()->loadMegolmSessions(room->id(), picklingMode()); } void Connection::saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session) + const QOlmInboundGroupSession& session) const { database()->saveMegolmSession(room->id(), session.sessionId(), session.pickle(picklingMode()), @@ -2211,64 +2231,179 @@ QStringList Connection::devicesForUser(const QString& userId) const return d->deviceKeys[userId].keys(); } -QString Connection::curveKeyForUserDevice(const QString& userId, - const QString& device) const +QString Connection::Private::curveKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[userId][device].keys["curve25519:" % device]; + return deviceKeys[userId][device].keys["curve25519:" % device]; } -QString Connection::edKeyForUserDevice(const QString& userId, - const QString& device) const +QString Connection::Private::edKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[userId][device].keys["ed25519:" % device]; + return deviceKeys[userId][device].keys["ed25519:" % device]; } -bool Connection::isKnownCurveKey(const QString& userId, - const QString& curveKey) const +bool Connection::Private::isKnownCurveKey(const QString& userId, + const QString& curveKey) const { - auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey")); + auto query = database->prepareQuery( + QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId " + "AND curveKey=:curveKey")); query.bindValue(":matrixId", userId); query.bindValue(":curveKey", curveKey); - database()->execute(query); + database->execute(query); return query.next(); } -bool Connection::hasOlmSession(const QString& user, const QString& deviceId) const +bool Connection::hasOlmSession(const QString& user, + const QString& deviceId) const { - const auto& curveKey = curveKeyForUserDevice(user, deviceId); + const auto& curveKey = d->curveKeyForUserDevice(user, deviceId); return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } -std::pair Connection::olmEncryptMessage( - const QString& userId, const QString& device, const QByteArray& message) const +std::pair Connection::Private::olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const { const auto& curveKey = curveKeyForUserDevice(userId, device); - QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); - const auto result = d->olmSessions[curveKey][0]->encrypt(message); - if (const auto pickle = - d->olmSessions[curveKey][0]->pickle(picklingMode())) { - database()->updateOlmSession(curveKey, - d->olmSessions[curveKey][0]->sessionId(), - *pickle); + const auto& olmSession = olmSessions.at(curveKey).front(); + QOlmMessage::Type type = olmSession->encryptMessageType(); + const auto result = olmSession->encrypt(message); + if (const auto pickle = olmSession->pickle(picklingMode)) { + database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle); } else { - qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); + qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } return { type, result.toCiphertext() }; } -void Connection::createOlmSession(const QString& theirIdentityKey, - const QString& theirOneTimeKey) const -{ - auto session = QOlmSession::createOutboundSession(olmAccount(), - theirIdentityKey, - theirOneTimeKey); +bool Connection::Private::createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const QJsonObject& oneTimeKeyObject) +{ + static QOlmUtility verifier; + qDebug(E2EE) << "Creating a new session for" << targetUserId + << targetDeviceId; + if (oneTimeKeyObject.isEmpty()) { + qWarning(E2EE) << "No one time key for" << targetUserId + << targetDeviceId; + return false; + } + auto signedOneTimeKey = oneTimeKeyObject.constBegin()->toObject(); + // Verify contents of signedOneTimeKey - for that, drop `signatures` and + // `unsigned` and then verify the object against the respective signature + const auto signature = + signedOneTimeKey.take("signatures"_ls)[targetUserId]["ed25519:"_ls % targetDeviceId] + .toString() + .toLatin1(); + signedOneTimeKey.remove("unsigned"_ls); + if (!verifier.ed25519Verify( + edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), + QJsonDocument(signedOneTimeKey).toJson(QJsonDocument::Compact), + signature)) { + qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId + << targetDeviceId << ". Skipping this device."; + return false; + } + const auto recipientCurveKey = + curveKeyForUserDevice(targetUserId, targetDeviceId); + auto session = + QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, + signedOneTimeKey["key"].toString()); if (!session) { qCWarning(E2EE) << "Failed to create olm session for " - << theirIdentityKey << session.error(); + << recipientCurveKey << session.error(); + return false; + } + saveSession(**session, recipientCurveKey); + olmSessions[recipientCurveKey].push_back(std::move(*session)); + return true; +} + +std::unique_ptr Connection::Private::makeEventForSessionKey( + const QString& roomId, const QString& targetUserId, + const QString& targetDeviceId, const QByteArray& sessionId, + const QByteArray& sessionKey) const +{ + // Noisy but nice for debugging + // qDebug(E2EE) << "Creating the payload for" << data->userId() << device << + // sessionId << sessionKey.toHex(); + const auto event = makeEvent("m.megolm.v1.aes-sha2", roomId, + sessionId, sessionKey, + data->userId()); + auto payloadJson = event->fullJson(); + payloadJson.insert("recipient"_ls, targetUserId); + payloadJson.insert(SenderKeyL, data->userId()); + payloadJson.insert("recipient_keys"_ls, + QJsonObject { { Ed25519Key, + edKeyForUserDevice(targetUserId, + targetDeviceId) } }); + payloadJson.insert("keys"_ls, + QJsonObject { + { Ed25519Key, + QString(olmAccount->identityKeys().ed25519) } }); + payloadJson.insert("sender_device"_ls, data->deviceId()); + + const auto [type, cipherText] = olmEncryptMessage( + targetUserId, targetDeviceId, + QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted { + { curveKeyForUserDevice(targetUserId, targetDeviceId), + QJsonObject { { "type"_ls, type }, + { "body"_ls, QString(cipherText) } } } + }; + + return makeEvent(encrypted, + olmAccount->identityKeys().curve25519); +} + +void Connection::sendSessionKeyToDevices( + const QString& roomId, 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 (!hasOlmSession(userId, deviceId)) { + hash[userId].insert(deviceId, "signed_curve25519"_ls); + qDebug(E2EE) << "Adding" << userId << deviceId + << "to keys to claim"; + } + + if (hash.isEmpty()) return; - } - d->saveSession(**session, theirIdentityKey); - d->olmSessions[theirIdentityKey].push_back(std::move(*session)); + + auto job = callApi(hash); + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { + UsersToDevicesToEvents usersToDevicesToEvents; + const auto oneTimeKeys = job->oneTimeKeys(); + for (const auto& [targetUserId, targetDeviceId] : + asKeyValueRange(devices)) { + if (!hasOlmSession(targetUserId, targetDeviceId) + && !d->createOlmSession( + targetUserId, targetDeviceId, + oneTimeKeys[targetUserId][targetDeviceId])) + continue; + + usersToDevicesToEvents[targetUserId][targetDeviceId] = + d->makeEventForSessionKey(roomId, targetUserId, targetDeviceId, + sessionId, sessionKey); + } + if (!usersToDevicesToEvents.empty()) { + sendToDevices(EncryptedEvent::TypeId, usersToDevicesToEvents); + QVector> receivedDevices; + receivedDevices.reserve(devices.size()); + for (const auto& [user, device] : asKeyValueRange(devices)) + receivedDevices.push_back( + { user, device, d->curveKeyForUserDevice(user, device) }); + + database()->setDevicesReceivedKey(roomId, receivedDevices, + sessionId, index); + } + }); } QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession( diff --git a/lib/connection.h b/lib/connection.h index a2824744..5b806350 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -319,22 +319,26 @@ public: #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; Database* database() const; + PicklingMode picklingMode() const; UnorderedMap loadRoomMegolmSessions( - const Room* room); + const Room* room) const; void saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session); + const QOlmInboundGroupSession& session) const; bool hasOlmSession(const QString& user, const QString& deviceId) const; QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( const QString& roomId) const; - void saveCurrentOutboundMegolmSession(const QString& roomId, const QOlmOutboundGroupSession &session) const; - - //This assumes that an olm session with (user, device) exists - std::pair olmEncryptMessage( - const QString& userId, const QString& device, - const QByteArray& message) const; - void createOlmSession(const QString& theirIdentityKey, - const QString& theirOneTimeKey) const; + void saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const; + + void sendSessionKeyToDevices(const QString& roomId, + const QByteArray& sessionId, + const QByteArray& sessionKey, + const QMultiHash& devices, + int index); + + QJsonObject decryptNotification(const QJsonObject ¬ification); + QStringList devicesForUser(const QString& userId) const; #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; @@ -695,16 +699,8 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); - PicklingMode picklingMode() const; - QJsonObject decryptNotification(const QJsonObject ¬ification); - - QStringList devicesForUser(const QString& userId) const; - QString curveKeyForUserDevice(const QString& userId, - const QString& device) const; - QString edKeyForUserDevice(const QString& userId, - const QString& device) const; - bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; #endif + Q_SIGNALS: /// \brief Initial server resolution has failed /// diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index ce1ca9ed..bcf1ad41 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -207,9 +207,9 @@ public: /// /// See the [key algorithms](/client-server-api/#key-algorithms) section for /// information on the Key Object format. - QHash> oneTimeKeys() const + QHash> oneTimeKeys() const { - return loadFromJson>>( + return loadFromJson>>( "one_time_keys"_ls); } }; diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 2b149aac..2a98d5d8 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -96,12 +96,13 @@ QOlmExpected QOlmSession::createOutboundSession( return std::make_unique(olmOutboundSession); } -QOlmExpected QOlmSession::pickle(const PicklingMode &mode) +QOlmExpected QOlmSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); QByteArray key = toKey(mode); const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); + pickledBuf.data(), + pickledBuf.length()); if (error == olm_error()) { return lastError(m_session); diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index faae16ef..021092c7 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -31,7 +31,7 @@ public: const QString& theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. - QOlmExpected pickle(const PicklingMode &mode); + QOlmExpected pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. static QOlmExpected unpickle( diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 2bda3086..3093db41 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -12,7 +12,9 @@ public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) explicit RoomKeyEvent(const QJsonObject& obj); - explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId); + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey, + const QString& senderId); QString algorithm() const { return contentPart("algorithm"_ls); } QString roomId() const { return contentPart(RoomIdKeyL); } 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 bbc203f267961469251a3e04045b455480f4daa4 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 29 May 2022 11:52:04 +0200 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/accountregistry.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 9b6114d9..91f2e9d7 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -21,7 +21,7 @@ void AccountRegistry::add(Connection* a) beginInsertRows(QModelIndex(), size(), size()); push_back(a); endInsertRows(); - Q_EMIT accountCountChanged(); + emit accountCountChanged(); } void AccountRegistry::drop(Connection* a) @@ -76,7 +76,7 @@ QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const Q auto job = new QKeychain::ReadPasswordJob(qAppName(), this); job->setKey(userId); - connect(job, &QKeychain::Job::finished, this, [job]() { + connect(job, &QKeychain::Job::finished, this, [job] { if (job->error() == QKeychain::Error::NoError) { return; } @@ -93,7 +93,7 @@ void AccountRegistry::invokeLogin() for (const auto& accountId : accounts) { AccountSettings account{accountId}; m_accountsLoading += accountId; - Q_EMIT accountsLoadingChanged(); + emit accountsLoadingChanged(); if (!account.homeserver().isEmpty()) { auto accessTokenLoadingJob = loadAccessTokenFromKeychain(account.userId()); -- cgit v1.2.3 From afcad0615b26421e60b234dc8488b6ea1150d416 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 29 May 2022 12:13:38 +0200 Subject: Fix CI --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7900235c..e507ae60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,6 +316,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) find_dependency(${Qt}Sql)") # For QuotientConfig.cmake.in endif() +target_include_directories(${PROJECT_NAME} PRIVATE ${QTKEYCHAIN_INCLUDE_DIRS}) target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES}) if (Qt STREQUAL Qt5) # See #483 -- cgit v1.2.3 From 283f95e429917bd0c7fb5982ceac1602eb2af0b9 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 29 May 2022 14:40:00 +0200 Subject: Error handling --- lib/accountregistry.cpp | 21 +++------------------ lib/accountregistry.h | 9 +++++++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 91f2e9d7..5322ee80 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -7,11 +7,6 @@ #include "connection.h" #include -#if QT_VERSION_MAJOR >= 6 -# include -#else -# include -#endif using namespace Quotient; void AccountRegistry::add(Connection* a) @@ -75,13 +70,6 @@ QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const Q qCDebug(MAIN) << "Reading access token from keychain for" << userId; auto job = new QKeychain::ReadPasswordJob(qAppName(), this); job->setKey(userId); - - connect(job, &QKeychain::Job::finished, this, [job] { - if (job->error() == QKeychain::Error::NoError) { - return; - } - //TODO error handling - }); job->start(); return job; @@ -100,7 +88,7 @@ void AccountRegistry::invokeLogin() connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob]() { AccountSettings account{accountId}; if (accessTokenLoadingJob->error() != QKeychain::Error::NoError) { - //TODO error handling + emit keychainError(accessTokenLoadingJob->error()); return; } @@ -111,11 +99,8 @@ void AccountRegistry::invokeLogin() connection->syncLoop(); }); - connect(connection, &Connection::loginError, this, [](const QString& error, const QString&) { - //TODO error handling - }); - connect(connection, &Connection::networkError, this, [](const QString& error, const QString&, int, int) { - //TODO error handling + connect(connection, &Connection::loginError, this, [this, connection](const QString& error, const QString& details) { + emit loginError(connection, error, details); }); connection->assumeIdentity(account.userId(), accessTokenLoadingJob->binaryData(), account.deviceId()); }); diff --git a/lib/accountregistry.h b/lib/accountregistry.h index ab337303..99827b73 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -9,6 +9,12 @@ #include +#if QT_VERSION_MAJOR >= 6 +# include +#else +# include +#endif + namespace QKeychain { class ReadPasswordJob; } @@ -69,6 +75,9 @@ public: Q_SIGNALS: void accountCountChanged(); void accountsLoadingChanged(); + + void keychainError(QKeychain::Error error); + void loginError(Connection* connection, QString message, QString details); private: QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId); QStringList m_accountsLoading; -- cgit v1.2.3 From fc01bc08fafe82b254f7562b55209c872297fac6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 29 May 2022 08:52:20 +0200 Subject: Refresh and organise run-tests.sh/adjust-config.sh run-tests.sh now uses the latest version of Synapse and has less repetitive code; adjust-config.sh moved to autotests/ (it had nothing specific to CI, after all), works with the newest Synapse (that has an additional enable_registration_without_verification safeguard) and no more depends on the config directory being called "data" but rather should be called from inside that directory (for the case when it is used separately from run-tests.sh and the config directory is not called "data"). --- .ci/adjust-config.sh | 53 -------------------------------------------- autotests/adjust-config.sh | 55 ++++++++++++++++++++++++++++++++++++++++++++++ autotests/run-tests.sh | 30 ++++++++++++------------- 3 files changed, 69 insertions(+), 69 deletions(-) delete mode 100755 .ci/adjust-config.sh create mode 100644 autotests/adjust-config.sh diff --git a/.ci/adjust-config.sh b/.ci/adjust-config.sh deleted file mode 100755 index b2ca52b2..00000000 --- a/.ci/adjust-config.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -CMD="" - -$CMD perl -pi -w -e \ - 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g;' data/homeserver.yaml -$CMD perl -pi -w -e \ - 's/rc_message_burst_count.*/rc_message_burst_count: 10000/g;' data/homeserver.yaml - -( -cat <&1 >/dev/null; trap - EXIT" EXIT echo Waiting for synapse to start... until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done echo Register alice -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice1 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice2 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice3 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice4 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice5 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice6 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice7 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice8 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u alice9 -p secret -c /data/homeserver.yaml https://localhost:8008' +for i in 1 2 3 4 5 6 7 8 9; do + docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u alice$i -p secret -c /data/homeserver.yaml https://localhost:8008" +done echo Register bob -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob1 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob2 -p secret -c /data/homeserver.yaml https://localhost:8008' -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u bob3 -p secret -c /data/homeserver.yaml https://localhost:8008' +for i in 1 2 3; do + docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u bob$i -p secret -c /data/homeserver.yaml https://localhost:8008" +done echo Register carl -docker exec synapse /bin/sh -c 'register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008' +docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008" GTEST_COLOR=1 ctest --verbose "$@" + -- cgit v1.2.3 From 886dda59307672f88c38a8555e9bfe67535d953f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 29 May 2022 20:07:14 +0200 Subject: TestOlmAccount: fix tests Mainly the change is about eliminating the checks for an exact number of key-value pairs inside `one_time_key_counts` - these checks started failing with new Synapse throwing `signed_curve25519: 0` into this dict. --- autotests/testolmaccount.cpp | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index e31ff6d3..3b19e7c8 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -201,10 +201,13 @@ void TestOlmAccount::uploadIdentityKey() OneTimeKeys unused; auto request = olmAccount->createUploadKeyRequest(unused); connect(request, &BaseJob::result, this, [request, conn] { - QCOMPARE(request->oneTimeKeyCounts().size(), 0); - }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); + if (!request->status().good()) + QFAIL("upload failed"); + const auto& oneTimeKeyCounts = request->oneTimeKeyCounts(); + // Allow the response to have entries with zero counts + QCOMPARE(std::accumulate(oneTimeKeyCounts.begin(), + oneTimeKeyCounts.end(), 0), + 0); }); conn->run(request); QSignalSpy spy3(request, &BaseJob::result); @@ -228,12 +231,10 @@ void TestOlmAccount::uploadOneTimeKeys() } auto request = new UploadKeysJob(none, oneTimeKeysHash); connect(request, &BaseJob::result, this, [request, conn] { - QCOMPARE(request->oneTimeKeyCounts().size(), 1); + if (!request->status().good()) + QFAIL("upload failed"); QCOMPARE(request->oneTimeKeyCounts().value(Curve25519Key), 5); }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); conn->run(request); QSignalSpy spy3(request, &BaseJob::result); QVERIFY(spy3.wait(10000)); @@ -256,12 +257,10 @@ void TestOlmAccount::uploadSignedOneTimeKeys() } auto request = new UploadKeysJob(none, oneTimeKeysHash); connect(request, &BaseJob::result, this, [request, nKeys, conn] { - QCOMPARE(request->oneTimeKeyCounts().size(), 1); + if (!request->status().good()) + QFAIL("upload failed"); QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), nKeys); }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); conn->run(request); QSignalSpy spy3(request, &BaseJob::result); QVERIFY(spy3.wait(10000)); @@ -276,12 +275,10 @@ void TestOlmAccount::uploadKeys() auto otks = olmAccount->oneTimeKeys(); auto request = olmAccount->createUploadKeyRequest(otks); connect(request, &BaseJob::result, this, [request, conn] { - QCOMPARE(request->oneTimeKeyCounts().size(), 1); + if (!request->status().good()) + QFAIL("upload failed"); QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); - connect(request, &BaseJob::failure, this, [] { - QFAIL("upload failed"); - }); conn->run(request); QSignalSpy spy3(request, &BaseJob::result); QVERIFY(spy3.wait(10000)); @@ -297,7 +294,6 @@ void TestOlmAccount::queryTest() aliceOlm->generateOneTimeKeys(1); auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); connect(aliceRes, &BaseJob::result, this, [aliceRes] { - QCOMPARE(aliceRes->oneTimeKeyCounts().size(), 1); QCOMPARE(aliceRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); QSignalSpy spy(aliceRes, &BaseJob::result); @@ -308,7 +304,6 @@ void TestOlmAccount::queryTest() bobOlm->generateOneTimeKeys(1); auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); connect(bobRes, &BaseJob::result, this, [bobRes] { - QCOMPARE(bobRes->oneTimeKeyCounts().size(), 1); QCOMPARE(bobRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); QSignalSpy spy1(bobRes, &BaseJob::result); @@ -368,7 +363,6 @@ void TestOlmAccount::claimKeys() auto request = bobOlm->createUploadKeyRequest(bobOlm->oneTimeKeys()); connect(request, &BaseJob::result, this, [request, bob] { - QCOMPARE(request->oneTimeKeyCounts().size(), 1); QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1); }); bob->run(request); @@ -434,7 +428,6 @@ void TestOlmAccount::claimMultipleKeys() auto res = olm->createUploadKeyRequest(olm->oneTimeKeys()); QSignalSpy spy(res, &BaseJob::result); connect(res, &BaseJob::result, this, [res] { - QCOMPARE(res->oneTimeKeyCounts().size(), 1); QCOMPARE(res->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice->run(res); @@ -445,7 +438,6 @@ void TestOlmAccount::claimMultipleKeys() auto res1 = olm1->createUploadKeyRequest(olm1->oneTimeKeys()); QSignalSpy spy1(res1, &BaseJob::result); connect(res1, &BaseJob::result, this, [res1] { - QCOMPARE(res1->oneTimeKeyCounts().size(), 1); QCOMPARE(res1->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice1->run(res1); @@ -456,7 +448,6 @@ void TestOlmAccount::claimMultipleKeys() auto res2 = olm2->createUploadKeyRequest(olm2->oneTimeKeys()); QSignalSpy spy2(res2, &BaseJob::result); connect(res2, &BaseJob::result, this, [res2] { - QCOMPARE(res2->oneTimeKeyCounts().size(), 1); QCOMPARE(res2->oneTimeKeyCounts().value(SignedCurve25519Key), 10); }); alice2->run(res2); @@ -480,7 +471,6 @@ void TestOlmAccount::claimMultipleKeys() QVERIFY(jobSpy.wait(10000)); const auto userId = alice->userId(); - QCOMPARE(job->oneTimeKeys().size(), 1); QCOMPARE(job->oneTimeKeys().value(userId).size(), 3); } -- cgit v1.2.3 From 9c436d314f6b082287fb129ea1fcf960313e5c27 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 29 May 2022 22:21:53 +0200 Subject: Also reemit resolveError --- lib/accountregistry.cpp | 3 +++ lib/accountregistry.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 5322ee80..9b148b51 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -102,6 +102,9 @@ void AccountRegistry::invokeLogin() connect(connection, &Connection::loginError, this, [this, connection](const QString& error, const QString& details) { emit loginError(connection, error, details); }); + connect(connection, &Connection::resolveError, this, [this, connection](QString error) { + emit resolveError(connection, error); + }); connection->assumeIdentity(account.userId(), accessTokenLoadingJob->binaryData(), account.deviceId()); }); } diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 99827b73..38cfe6c6 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -78,6 +78,8 @@ Q_SIGNALS: void keychainError(QKeychain::Error error); void loginError(Connection* connection, QString message, QString details); + void resolveError(Connection* connection, QString error); + private: QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId); QStringList m_accountsLoading; -- cgit v1.2.3 From 8a868988be0c5ce4ea8a22f4a6f8c7e8b2e42037 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 29 May 2022 23:01:09 +0200 Subject: run-tests.sh: fix weird CI failure on Linux/Clang Also fix a leftover data/ prefix in adjust-config.sh --- autotests/adjust-config.sh | 2 +- autotests/run-tests.sh | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/autotests/adjust-config.sh b/autotests/adjust-config.sh index a55ac670..68ea58ab 100644 --- a/autotests/adjust-config.sh +++ b/autotests/adjust-config.sh @@ -36,7 +36,7 @@ rc_joins: per_second: 10000 burst_count: 100000 HEREDOC -) | $CMD tee -a data/homeserver.yaml +) | $CMD tee -a homeserver.yaml $CMD perl -pi -w -e \ 's/^#enable_registration: false/enable_registration: true/g;' homeserver.yaml diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index a3ee2ade..05a215af 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -4,9 +4,7 @@ chmod 0777 data rm ~/.local/share/testolmaccount -rf docker run -v `pwd`/data:/data --rm \ -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate -pushd data -. ../autotests/adjust-config.sh -popd +(cd data && . ../autotests/adjust-config.sh) docker run -d \ --name synapse \ -p 1234:8008 \ -- cgit v1.2.3 From 999ec716d5e2b03aceb562e730edf3939eb2578a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 30 May 2022 15:23:39 +0200 Subject: Emit loggedOut() after the access token is gone ...not before. --- lib/connection.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 7e36b3c9..b7f49546 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -398,7 +398,7 @@ public: //TODO error handling } - void removeAccessTokenFromKeychain() + void dropAccessToken() { qCDebug(MAIN) << "Removing access token from keychain for" << q->userId(); auto job = new QKeychain::DeletePasswordJob(qAppName()); @@ -411,6 +411,8 @@ public: pickleJob->setKey(q->userId() + "-Pickle"_ls); pickleJob->start(); //TODO error handling + + data->setToken({}); } }; @@ -727,10 +729,9 @@ void Connection::logout() || d->logoutJob->error() == BaseJob::ContentAccessError) { if (d->syncLoopConnection) disconnect(d->syncLoopConnection); - d->data->setToken({}); - emit loggedOut(); SettingsGroup("Accounts").remove(userId()); - d->removeAccessTokenFromKeychain(); + d->dropAccessToken(); + emit loggedOut(); deleteLater(); } else { // logout() somehow didn't proceed - restore the session state emit stateChanged(); -- cgit v1.2.3 From 078f5aab2e1eb1dea30828429069836509551b07 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 30 May 2022 15:24:02 +0200 Subject: Cleanup and reformatting --- lib/accountregistry.cpp | 60 ++++++++++++++++++++++++++++--------------------- lib/connection.cpp | 10 +++------ 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 9b148b51..b40d5ecf 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -79,35 +79,45 @@ void AccountRegistry::invokeLogin() { const auto accounts = SettingsGroup("Accounts").childGroups(); for (const auto& accountId : accounts) { - AccountSettings account{accountId}; + AccountSettings account { accountId }; m_accountsLoading += accountId; emit accountsLoadingChanged(); - if (!account.homeserver().isEmpty()) { - auto accessTokenLoadingJob = loadAccessTokenFromKeychain(account.userId()); - connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob]() { - AccountSettings account{accountId}; - if (accessTokenLoadingJob->error() != QKeychain::Error::NoError) { - emit keychainError(accessTokenLoadingJob->error()); - return; - } - - auto connection = new Connection(account.homeserver()); - connect(connection, &Connection::connected, this, [connection] { - connection->loadState(); - connection->setLazyLoading(true); - - connection->syncLoop(); + if (account.homeserver().isEmpty()) + continue; + + auto accessTokenLoadingJob = + loadAccessTokenFromKeychain(account.userId()); + connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, + [accountId, this, accessTokenLoadingJob]() { + if (accessTokenLoadingJob->error() + != QKeychain::Error::NoError) { + emit keychainError(accessTokenLoadingJob->error()); + return; + } + + AccountSettings account { accountId }; + auto connection = new Connection(account.homeserver()); + connect(connection, &Connection::connected, this, + [connection] { + connection->loadState(); + connection->setLazyLoading(true); + + connection->syncLoop(); + }); + connect(connection, &Connection::loginError, this, + [this, connection](const QString& error, + const QString& details) { + emit loginError(connection, error, details); + }); + connect(connection, &Connection::resolveError, this, + [this, connection](const QString& error) { + emit resolveError(connection, error); + }); + connection->assumeIdentity( + account.userId(), accessTokenLoadingJob->binaryData(), + account.deviceId()); }); - connect(connection, &Connection::loginError, this, [this, connection](const QString& error, const QString& details) { - emit loginError(connection, error, details); - }); - connect(connection, &Connection::resolveError, this, [this, connection](QString error) { - emit resolveError(connection, error); - }); - connection->assumeIdentity(account.userId(), accessTokenLoadingJob->binaryData(), account.deviceId()); - }); - } } } diff --git a/lib/connection.cpp b/lib/connection.cpp index b7f49546..102fb16d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -387,7 +387,7 @@ public: const QByteArray& sessionKey) const; #endif - void saveAccessTokenToKeychain() + void saveAccessTokenToKeychain() const { qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); auto job = new QKeychain::WritePasswordJob(qAppName()); @@ -593,11 +593,9 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) data->setDeviceId(loginJob->deviceId()); completeSetup(loginJob->userId()); saveAccessTokenToKeychain(); -#ifndef Quotient_E2EE_ENABLED - qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; -#else // Quotient_E2EE_ENABLED +#ifdef Quotient_E2EE_ENABLED database->clear(); -#endif // Quotient_E2EE_ENABLED +#endif }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -654,9 +652,7 @@ void Connection::Private::completeSetup(const QString& mxId) olmAccount = std::make_unique(data->userId(), data->deviceId(), q); connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); -#ifdef Quotient_E2EE_ENABLED loadSessions(); -#endif if (database->accountPickle().isEmpty()) { // create new account and save unpickle data -- cgit v1.2.3 From 6e27a49fbbc58a7310753f882fe372ddb0f63e33 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 30 May 2022 17:41:41 +0200 Subject: CI: Build with QtKeychain 0.13.2 QtKeychain master suffers from https://github.com/frankosterfeld/qtkeychain/issues/213. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b704b3b9..c690745f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,7 +171,7 @@ jobs: - name: Build and install QtKeychain run: | cd .. - git clone https://github.com/frankosterfeld/qtkeychain.git + git clone -b v0.13.2 https://github.com/frankosterfeld/qtkeychain.git cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS cmake --build qtkeychain/build --target install -- cgit v1.2.3 From 05067556a295dc5e01a97a54c625a1776611c8dd Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 May 2022 00:08:48 +0200 Subject: Save connection state when destructing accountregistry --- lib/accountregistry.cpp | 7 +++++++ lib/accountregistry.h | 2 ++ 2 files changed, 9 insertions(+) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index b40d5ecf..ab8c46e3 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -125,3 +125,10 @@ QStringList AccountRegistry::accountsLoading() const { return m_accountsLoading; } + +AccountRegistry::~AccountRegistry() +{ + for (const auto& connection : *this) { + connection->saveState(); + } +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 38cfe6c6..959c7d42 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -42,6 +42,8 @@ public: [[deprecated("Use Accounts variable instead")]] // static AccountRegistry& instance(); + ~AccountRegistry(); + // Expose most of QVector's const-API but only provide add() and drop() // for changing it. In theory other changing operations could be supported // too; but then boilerplate begin/end*() calls has to be tucked into each -- cgit v1.2.3 From a7c43995c3a47bbbac8d862f69f9229eb39f4ed6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 31 May 2022 13:52:38 +0200 Subject: AccountRegistry: fix dropping an inexistent Connection On Debug builds this would lead to an assertion failure inside Qt. --- lib/accountregistry.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index b40d5ecf..b3025fa4 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -21,10 +21,11 @@ void AccountRegistry::add(Connection* a) void AccountRegistry::drop(Connection* a) { - const auto idx = indexOf(a); - beginRemoveRows(QModelIndex(), idx, idx); - remove(idx); - endRemoveRows(); + if (const auto idx = indexOf(a); idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + remove(idx); + endRemoveRows(); + } Q_ASSERT(!contains(a)); } -- cgit v1.2.3 From 3c6ca3c89d7b1a972d50ec4eb0b42ab350771f72 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 31 May 2022 17:36:17 +0200 Subject: Update gtad.yml to match v3 API definitions Also: use quotient-im/matrix-spec main branch again, now that it has adjusted definitions; drop un(der)used partials --- .github/workflows/ci.yml | 3 --- gtad/gtad.yaml | 17 +++++------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bea9f436..9b9383db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,9 +180,6 @@ jobs: working-directory: ${{ runner.workspace }} run: | git clone https://github.com/quotient-im/matrix-spec.git - pushd matrix-spec - git checkout fixcompilation - popd git clone --recursive https://github.com/KitsuneRal/gtad.git cmake -S gtad -B build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF cmake --build build/gtad diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 03c23886..e8d4ba35 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -85,9 +85,8 @@ analyzer: imports: '"events/eventloader.h"' +on: - /state_event.yaml$/: StateEventPtr - - /room_event.yaml$/: RoomEventPtr - - /event.yaml$/: EventPtr - - /m\.room\.member/: void # Skip resolving; see EventsArray<> below + - /(room|client)_event.yaml$/: RoomEventPtr + - /event(_without_room_id)?.yaml$/: EventPtr - +set: # This renderer actually applies to all $ref things _importRenderer: '"{{#segments}}{{_}}{{#_join}}/{{/_join}}{{/segments}}.h"' @@ -115,12 +114,9 @@ analyzer: - /^Notification|Result$/: type: "std::vector<{{1}}>" imports: '"events/eventloader.h"' - - /m\.room\.member/: # Only used in an array (see also above) - type: "EventsArray" - imports: '"events/roommemberevent.h"' - /state_event.yaml$/: StateEvents - - /room_event.yaml$/: RoomEvents - - /event.yaml$/: Events + - /(room|client)_event.yaml$/: RoomEvents + - /event(_without_room_id)?.yaml$/: Events - //: "QVector<{{1}}>" - map: # `additionalProperties` in OpenAPI - RoomState: @@ -164,11 +160,8 @@ mustache: qualifiedMaybeOmittableType: "{{>openOmittable}}{{dataType.qualifiedName}}{{>closeOmittable}}" - ref: "{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" maybeCrefType: - "{{#avoidCopy}}const {{/avoidCopy}}{{>maybeOmittableType}}{{>ref}}" - qualifiedMaybeCrefType: - "{{#avoidCopy}}const {{/avoidCopy}}{{>qualifiedMaybeOmittableType}}{{>ref}}" + "{{#avoidCopy}}const {{/avoidCopy}}{{>maybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}" maybeCrefJsonObject: "{{^propertyMap}}const QJsonObject&{{/propertyMap}}\ -- cgit v1.2.3 From fed831820965ad5654317d5e7df18c23fa75de20 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 31 May 2022 16:30:22 +0200 Subject: gtad.yaml (again): shortcut OneTimeKeys This requires OneTimeKeys in keys.yaml to be marked as such (done in quotient-im/matrix-spec but it's not there in matrix-org/matrix-spec). --- gtad/gtad.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index e8d4ba35..b1c143b6 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -106,6 +106,9 @@ analyzer: # imports: '"csapi/definitions/sync_filter.h"' # - getFilter<: *Filter - RoomFilter: # A structure inside Filter, same story as with *_filter.yaml + - OneTimeKeys: + type: OneTimeKeys + imports: '"e2ee/e2ee.h"' - //: *UseOmittable - array: - string: QStringList -- cgit v1.2.3 From 42811660094c88a4a1bfa8bd8ace5f4b148c246a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 31 May 2022 18:24:53 +0200 Subject: Regenerate API files (FTBFS; see the next commit) --- .../schema/core-event-schema/stripped_state.h | 44 +++++++++++ lib/csapi/account-data.cpp | 12 +-- lib/csapi/admin.cpp | 4 +- lib/csapi/administrative_contact.cpp | 18 ++--- lib/csapi/administrative_contact.h | 26 +++++-- lib/csapi/appservice_room_directory.cpp | 8 +- lib/csapi/appservice_room_directory.h | 3 +- lib/csapi/banning.cpp | 4 +- lib/csapi/capabilities.cpp | 4 +- lib/csapi/content-repo.cpp | 27 +++---- lib/csapi/content-repo.h | 3 +- lib/csapi/create_room.cpp | 2 +- lib/csapi/create_room.h | 46 ++++++----- lib/csapi/cross_signing.cpp | 8 +- lib/csapi/cross_signing.h | 10 ++- lib/csapi/definitions/auth_data.h | 7 +- lib/csapi/definitions/openid_token.h | 8 +- lib/csapi/definitions/public_rooms_chunk.h | 73 ++++++++++++++++++ lib/csapi/definitions/public_rooms_response.h | 74 +----------------- lib/csapi/definitions/push_condition.h | 4 +- lib/csapi/device_management.cpp | 14 ++-- lib/csapi/device_management.h | 3 +- lib/csapi/directory.cpp | 14 ++-- lib/csapi/event_context.cpp | 4 +- lib/csapi/filter.cpp | 6 +- lib/csapi/inviting.cpp | 2 +- lib/csapi/inviting.h | 2 +- lib/csapi/joining.cpp | 4 +- lib/csapi/joining.h | 8 +- lib/csapi/keys.cpp | 14 ++-- lib/csapi/keys.h | 33 ++++++-- lib/csapi/kicking.cpp | 2 +- lib/csapi/knocking.cpp | 2 +- lib/csapi/knocking.h | 2 +- lib/csapi/leaving.cpp | 6 +- lib/csapi/list_joined_rooms.cpp | 4 +- lib/csapi/list_public_rooms.cpp | 12 +-- lib/csapi/list_public_rooms.h | 10 +-- lib/csapi/login.cpp | 6 +- lib/csapi/logout.cpp | 8 +- lib/csapi/message_pagination.cpp | 17 +++-- lib/csapi/message_pagination.h | 55 +++++++++----- lib/csapi/notifications.cpp | 4 +- lib/csapi/notifications.h | 3 +- lib/csapi/openid.cpp | 2 +- lib/csapi/openid.h | 5 +- lib/csapi/peeking_events.cpp | 4 +- lib/csapi/peeking_events.h | 4 +- lib/csapi/presence.cpp | 6 +- lib/csapi/profile.cpp | 18 +++-- lib/csapi/pusher.cpp | 6 +- lib/csapi/pushrules.cpp | 26 +++---- lib/csapi/read_markers.cpp | 2 +- lib/csapi/receipts.cpp | 2 +- lib/csapi/redaction.cpp | 2 +- lib/csapi/registration.cpp | 21 +++--- lib/csapi/registration.h | 21 +++--- lib/csapi/registration_tokens.cpp | 33 ++++++++ lib/csapi/registration_tokens.h | 44 +++++++++++ lib/csapi/report_content.cpp | 2 +- lib/csapi/room_send.cpp | 2 +- lib/csapi/room_send.h | 3 +- lib/csapi/room_state.cpp | 2 +- lib/csapi/room_upgrades.cpp | 2 +- lib/csapi/rooms.cpp | 20 ++--- lib/csapi/rooms.h | 13 +--- lib/csapi/search.cpp | 2 +- lib/csapi/space_hierarchy.cpp | 43 +++++++++++ lib/csapi/space_hierarchy.h | 88 ++++++++++++++++++++++ lib/csapi/sso_login_redirect.cpp | 8 +- lib/csapi/tags.cpp | 10 +-- lib/csapi/third_party_lookup.cpp | 24 +++--- lib/csapi/third_party_membership.cpp | 2 +- lib/csapi/third_party_membership.h | 2 +- lib/csapi/to_device.cpp | 2 +- lib/csapi/typing.cpp | 2 +- lib/csapi/users.cpp | 2 +- lib/csapi/versions.h | 8 +- lib/csapi/voip.cpp | 4 +- lib/csapi/whoami.cpp | 4 +- lib/csapi/whoami.h | 8 ++ 81 files changed, 712 insertions(+), 357 deletions(-) create mode 100644 event-schemas/schema/core-event-schema/stripped_state.h create mode 100644 lib/csapi/definitions/public_rooms_chunk.h create mode 100644 lib/csapi/registration_tokens.cpp create mode 100644 lib/csapi/registration_tokens.h create mode 100644 lib/csapi/space_hierarchy.cpp create mode 100644 lib/csapi/space_hierarchy.h diff --git a/event-schemas/schema/core-event-schema/stripped_state.h b/event-schemas/schema/core-event-schema/stripped_state.h new file mode 100644 index 00000000..742b0a56 --- /dev/null +++ b/event-schemas/schema/core-event-schema/stripped_state.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { +/// A stripped down state event, with only the `type`, `state_key`, +/// `sender`, and `content` keys. +struct StrippedStateEvent { + /// The `content` for the event. + QJsonObject content; + + /// The `state_key` for the event. + QString stateKey; + + /// The `type` for the event. + QString type; + + /// The `sender` for the event. + QString sender; +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const StrippedStateEvent& pod) + { + addParam<>(jo, QStringLiteral("content"), pod.content); + addParam<>(jo, QStringLiteral("state_key"), pod.stateKey); + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<>(jo, QStringLiteral("sender"), pod.sender); + } + static void fillFrom(const QJsonObject& jo, StrippedStateEvent& pod) + { + fromJson(jo.value("content"_ls), pod.content); + fromJson(jo.value("state_key"_ls), pod.stateKey); + fromJson(jo.value("type"_ls), pod.type); + fromJson(jo.value("sender"_ls), pod.sender); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 09fc8d40..1343eb98 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -9,7 +9,7 @@ using namespace Quotient; SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)) { setRequestData(RequestData(toJson(content))); @@ -19,13 +19,13 @@ QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)); } GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)) {} @@ -34,7 +34,7 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)) { setRequestData(RequestData(toJson(content))); @@ -46,7 +46,7 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)); } @@ -55,6 +55,6 @@ GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)) {} diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 81dd0624..322212db 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -9,11 +9,11 @@ using namespace Quotient; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/admin/whois/", userId)); } GetWhoIsJob::GetWhoIsJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), - makePath("/_matrix/client/r0", "/admin/whois/", userId)) + makePath("/_matrix/client/v3", "/admin/whois/", userId)) {} diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index 589c9fc1..f52e2e1f 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -9,17 +9,17 @@ using namespace Quotient; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/account/3pid")); } GetAccount3PIDsJob::GetAccount3PIDsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), - makePath("/_matrix/client/r0", "/account/3pid")) + makePath("/_matrix/client/v3", "/account/3pid")) {} Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), - makePath("/_matrix/client/r0", "/account/3pid")) + makePath("/_matrix/client/v3", "/account/3pid")) { QJsonObject _data; addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); @@ -29,7 +29,7 @@ Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), - makePath("/_matrix/client/r0", "/account/3pid/add")) + makePath("/_matrix/client/v3", "/account/3pid/add")) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); @@ -41,7 +41,7 @@ Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer, const QString& idAccessToken, const QString& sid) : BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"), - makePath("/_matrix/client/r0", "/account/3pid/bind")) + makePath("/_matrix/client/v3", "/account/3pid/bind")) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); @@ -55,7 +55,7 @@ Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"), - makePath("/_matrix/client/r0", "/account/3pid/delete")) + makePath("/_matrix/client/v3", "/account/3pid/delete")) { QJsonObject _data; addParam(_data, QStringLiteral("id_server"), idServer); @@ -69,7 +69,7 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), - makePath("/_matrix/client/r0", "/account/3pid/unbind")) + makePath("/_matrix/client/v3", "/account/3pid/unbind")) { QJsonObject _data; addParam(_data, QStringLiteral("id_server"), idServer); @@ -82,7 +82,7 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/3pid/email/requestToken"), false) { @@ -92,7 +92,7 @@ RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/3pid/msisdn/requestToken"), false) { diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index e636b12a..27334850 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -128,6 +128,22 @@ public: * The third party credentials to associate with the account. */ explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds); + + // Result properties + + /// An optional field containing a URL where the client must + /// submit the validation token to, with identical parameters + /// to the Identity Service API's `POST + /// /validate/email/submitToken` endpoint (without the requirement + /// for an access token). The homeserver must send this token to the + /// user (if applicable), who should then be prompted to provide it + /// to the client. + /// + /// If this field is not present, the client can assume that + /// verification will happen without the client's involvement + /// provided the homeserver advertises this specification version + /// in the `/versions` response (ie: r0.5.0). + QUrl submitUrl() const { return loadFromJson("submit_url"_ls); } }; template <> @@ -235,7 +251,7 @@ public: /// An indicator as to whether or not the homeserver was able to unbind /// the 3PID from the identity server. `success` indicates that the - /// indentity server has unbound the identifier whereas `no-support` + /// identity server has unbound the identifier whereas `no-support` /// indicates that the identity server refuses to support the request /// or the homeserver was not able to determine an identity server to /// unbind from. @@ -295,7 +311,7 @@ public: * be used to request validation tokens when adding an email address to an * account. This API's parameters and response are identical to that of * the - * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * endpoint. The homeserver should validate * the email itself, either by sending a validation email itself or by using * a service it has control over. @@ -311,7 +327,7 @@ public: * be used to request validation tokens when adding an email address to an * account. This API's parameters and response are identical to that of * the - * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * endpoint. The homeserver should validate * the email itself, either by sending a validation email itself or by * using a service it has control over. @@ -337,7 +353,7 @@ public: * be used to request validation tokens when adding a phone number to an * account. This API's parameters and response are identical to that of * the - * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * endpoint. The homeserver should validate * the phone number itself, either by sending a validation message itself or by * using a service it has control over. @@ -353,7 +369,7 @@ public: * be used to request validation tokens when adding a phone number to an * account. This API's parameters and response are identical to that of * the - * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * endpoint. The homeserver should validate * the phone number itself, either by sending a validation message itself * or by using a service it has control over. diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index 40d784c6..c989559f 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -6,11 +6,13 @@ using namespace Quotient; -UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob( - const QString& networkId, const QString& roomId, const QString& visibility) +UpdateAppserviceRoomDirectoryVisibilityJob:: + UpdateAppserviceRoomDirectoryVisibilityJob(const QString& networkId, + const QString& roomId, + const QString& visibility) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"), - makePath("/_matrix/client/r0", "/directory/list/appservice/", + makePath("/_matrix/client/v3", "/directory/list/appservice/", networkId, "/", roomId)) { QJsonObject _data; diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index 6b2801ca..d6268979 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -21,8 +21,7 @@ namespace Quotient { * instead of a typical client's access_token. This API cannot be invoked by * users who are not identified as application services. */ -class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob - : public BaseJob { +class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { public: /*! \brief Updates a room's visibility in the application service's room * directory. diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 472128bb..77047e89 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -9,7 +9,7 @@ using namespace Quotient; BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/ban")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/ban")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); @@ -20,7 +20,7 @@ BanJob::BanJob(const QString& roomId, const QString& userId, UnbanJob::UnbanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/unban")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp index bc21e462..ca2a543f 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/capabilities")); } GetCapabilitiesJob::GetCapabilitiesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), - makePath("/_matrix/client/r0", "/capabilities")) + makePath("/_matrix/client/v3", "/capabilities")) { addExpectedKey("capabilities"); } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 6d1e38b6..7d740cb7 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -16,7 +16,7 @@ auto queryToUploadContent(const QString& filename) UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) : BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"), - makePath("/_matrix/media/r0", "/upload"), + makePath("/_matrix/media/v3", "/upload"), queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); @@ -35,7 +35,7 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", "/download/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId), queryToGetContent(allowRemote)); } @@ -43,7 +43,7 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"), - makePath("/_matrix/media/r0", "/download/", serverName, "/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId), queryToGetContent(allowRemote), {}, false) { @@ -64,7 +64,7 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", "/download/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId, "/", fileName), queryToGetContentOverrideName(allowRemote)); @@ -75,7 +75,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& fileName, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"), - makePath("/_matrix/media/r0", "/download/", serverName, "/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId, "/", fileName), queryToGetContentOverrideName(allowRemote), {}, false) { @@ -101,16 +101,17 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, { return BaseJob::makeRequestUrl( std::move(baseUrl), - makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", mediaId), + makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", mediaId), queryToGetContentThumbnail(width, height, method, allowRemote)); } GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, - const QString& mediaId, int width, - int height, const QString& method, + const QString& mediaId, + int width, int height, + const QString& method, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"), - makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", + makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", mediaId), queryToGetContentThumbnail(width, height, method, allowRemote), {}, false) @@ -130,24 +131,24 @@ QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", + makePath("/_matrix/media/v3", "/preview_url"), queryToGetUrlPreview(url, ts)); } GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable ts) : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), - makePath("/_matrix/media/r0", "/preview_url"), + makePath("/_matrix/media/v3", "/preview_url"), queryToGetUrlPreview(url, ts)) {} QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", "/config")); + makePath("/_matrix/media/v3", "/config")); } GetConfigJob::GetConfigJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), - makePath("/_matrix/media/r0", "/config")) + makePath("/_matrix/media/v3", "/config")) {} diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index 511db985..2ba66a35 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -162,7 +162,8 @@ public: * * \param method * The desired resizing method. See the - * [Thumbnails](/client-server-api/#thumbnails) section for more information. + * [Thumbnails](/client-server-api/#thumbnails) section for more + * information. * * \param allowRemote * Indicates to the server that it should not attempt to fetch diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 9aaef87f..834d8c13 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -16,7 +16,7 @@ CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& preset, Omittable isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), - makePath("/_matrix/client/r0", "/createRoom")) + makePath("/_matrix/client/v3", "/createRoom")) { QJsonObject _data; addParam(_data, QStringLiteral("visibility"), visibility); diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 7d566057..336b9767 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -26,16 +26,18 @@ namespace Quotient { * (and not other members) permission to send state events. Overridden * by the `power_level_content_override` parameter. * - * 4. Events set by the `preset`. Currently these are the `m.room.join_rules`, + * 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + * + * 5. Events set by the `preset`. Currently these are the `m.room.join_rules`, * `m.room.history_visibility`, and `m.room.guest_access` state events. * - * 5. Events listed in `initial_state`, in the order that they are + * 6. Events listed in `initial_state`, in the order that they are * listed. * - * 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` + * 7. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` * state events). * - * 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with + * 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with * `membership: invite` and `m.room.third_party_invite`). * * The available presets do the following with respect to room state: @@ -73,17 +75,20 @@ public: /// (and not other members) permission to send state events. Overridden /// by the `power_level_content_override` parameter. /// - /// 4. Events set by the `preset`. Currently these are the + /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + /// + /// 5. Events set by the `preset`. Currently these are the /// `m.room.join_rules`, /// `m.room.history_visibility`, and `m.room.guest_access` state events. /// - /// 5. Events listed in `initial_state`, in the order that they are + /// 6. Events listed in `initial_state`, in the order that they are /// listed. /// - /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` + /// 7. Events implied by `name` and `topic` (`m.room.name` and + /// `m.room.topic` /// state events). /// - /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` + /// 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member` /// with /// `membership: invite` and `m.room.third_party_invite`). /// @@ -132,17 +137,20 @@ public: /// (and not other members) permission to send state events. Overridden /// by the `power_level_content_override` parameter. /// - /// 4. Events set by the `preset`. Currently these are the + /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + /// + /// 5. Events set by the `preset`. Currently these are the /// `m.room.join_rules`, /// `m.room.history_visibility`, and `m.room.guest_access` state events. /// - /// 5. Events listed in `initial_state`, in the order that they are + /// 6. Events listed in `initial_state`, in the order that they are /// listed. /// - /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` + /// 7. Events implied by `name` and `topic` (`m.room.name` and + /// `m.room.topic` /// state events). /// - /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` + /// 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member` /// with /// `membership: invite` and `m.room.third_party_invite`). /// @@ -190,7 +198,8 @@ public: * would be `#foo:example.com`. * * The complete room alias will become the canonical alias for - * the room. + * the room and an `m.room.canonical_alias` event will be sent + * into the room. * * \param name * If this is included, an `m.room.name` event will be sent @@ -218,9 +227,10 @@ public: * * \param creationContent * Extra keys, such as `m.federate`, to be added to the content - * of the [`m.room.create`](client-server-api/#mroomcreate) event. The - * server will clobber the following keys: `creator`, `room_version`. Future - * versions of the specification may allow the server to clobber other keys. + * of the [`m.room.create`](/client-server-api/#mroomcreate) event. The + * server will overwrite the following keys: `creator`, `room_version`. + * Future versions of the specification may allow the server to overwrite + * other keys. * * \param initialState * A list of state events to set in the new room. This allows @@ -229,7 +239,7 @@ public: * with type, state_key and content keys set. * * Takes precedence over events set by `preset`, but gets - * overriden by `name` and `topic` keys. + * overridden by `name` and `topic` keys. * * \param preset * Convenience parameter for setting various default state events @@ -249,7 +259,7 @@ public: * \param powerLevelContentOverride * The power level content to override in the default power level * event. This object is applied on top of the generated - * [`m.room.power_levels`](client-server-api/#mroompower_levels) + * [`m.room.power_levels`](/client-server-api/#mroompower_levels) * event content prior to it being sent to the room. Defaults to * overriding nothing. */ diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp index 1fa0e949..c6c34772 100644 --- a/lib/csapi/cross_signing.cpp +++ b/lib/csapi/cross_signing.cpp @@ -9,9 +9,10 @@ using namespace Quotient; UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( const Omittable& masterKey, const Omittable& selfSigningKey, - const Omittable& userSigningKey) + const Omittable& userSigningKey, + const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), - makePath("/_matrix/client/r0", "/keys/device_signing/upload")) + makePath("/_matrix/client/v3", "/keys/device_signing/upload")) { QJsonObject _data; addParam(_data, QStringLiteral("master_key"), masterKey); @@ -19,13 +20,14 @@ UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( selfSigningKey); addParam(_data, QStringLiteral("user_signing_key"), userSigningKey); + addParam(_data, QStringLiteral("auth"), auth); setRequestData(std::move(_data)); } UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( const QHash>& signatures) : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), - makePath("/_matrix/client/r0", "/keys/signatures/upload")) + makePath("/_matrix/client/v3", "/keys/signatures/upload")) { setRequestData(RequestData(toJson(signatures))); } diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h index 617b61d1..6cea73e6 100644 --- a/lib/csapi/cross_signing.h +++ b/lib/csapi/cross_signing.h @@ -4,6 +4,7 @@ #pragma once +#include "csapi/definitions/auth_data.h" #include "csapi/definitions/cross_signing_key.h" #include "jobs/basejob.h" @@ -35,11 +36,16 @@ public: * the accompanying master key, or by the user\'s most recently * uploaded master key if no master key is included in the * request. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. */ explicit UploadCrossSigningKeysJob( const Omittable& masterKey = none, const Omittable& selfSigningKey = none, - const Omittable& userSigningKey = none); + const Omittable& userSigningKey = none, + const Omittable& auth = none); }; /*! \brief Upload cross-signing signatures. @@ -55,7 +61,7 @@ public: * The signatures to be published. */ explicit UploadCrossSigningSignaturesJob( - const QHash>& signatures = {}); + const QHash>& signatures); // Result properties diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h index e92596d0..a9972323 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -10,7 +10,10 @@ namespace Quotient { /// Used by clients to submit authentication information to the /// interactive-authentication API struct AuthenticationData { - /// The login type that the client is attempting to complete. + /// The authentication type that the client is attempting to complete. + /// May be omitted if `session` is given, and the client is reissuing a + /// request which it believes has been completed out-of-band (for example, + /// via the [fallback mechanism](#fallback)). QString type; /// The value of the session key given by the homeserver. @@ -25,7 +28,7 @@ struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const AuthenticationData& pod) { fillJson(jo, pod.authInfo); - addParam<>(jo, QStringLiteral("type"), pod.type); + addParam(jo, QStringLiteral("type"), pod.type); addParam(jo, QStringLiteral("session"), pod.session); } static void fillFrom(QJsonObject jo, AuthenticationData& pod) diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h index 3c447321..9b026dea 100644 --- a/lib/csapi/definitions/openid_token.h +++ b/lib/csapi/definitions/openid_token.h @@ -8,7 +8,7 @@ namespace Quotient { -struct OpenidToken { +struct OpenIdCredentials { /// An access token the consumer may use to verify the identity of /// the person who generated the token. This is given to the federation /// API `GET /openid/userinfo` to verify the user's identity. @@ -27,8 +27,8 @@ struct OpenidToken { }; template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const OpenidToken& pod) +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const OpenIdCredentials& pod) { addParam<>(jo, QStringLiteral("access_token"), pod.accessToken); addParam<>(jo, QStringLiteral("token_type"), pod.tokenType); @@ -36,7 +36,7 @@ struct JsonObjectConverter { pod.matrixServerName); addParam<>(jo, QStringLiteral("expires_in"), pod.expiresIn); } - static void fillFrom(const QJsonObject& jo, OpenidToken& pod) + static void fillFrom(const QJsonObject& jo, OpenIdCredentials& pod) { fromJson(jo.value("access_token"_ls), pod.accessToken); fromJson(jo.value("token_type"_ls), pod.tokenType); diff --git a/lib/csapi/definitions/public_rooms_chunk.h b/lib/csapi/definitions/public_rooms_chunk.h new file mode 100644 index 00000000..eac68213 --- /dev/null +++ b/lib/csapi/definitions/public_rooms_chunk.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { + +struct PublicRoomsChunk { + /// The canonical alias of the room, if any. + QString canonicalAlias; + + /// The name of the room, if any. + QString name; + + /// The number of members joined to the room. + int numJoinedMembers; + + /// The ID of the room. + QString roomId; + + /// The topic of the room, if any. + QString topic; + + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; + + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; + + /// The URL for the room's avatar, if one is set. + QUrl avatarUrl; + + /// The room's join rule. When not present, the room is assumed to + /// be `public`. + QString joinRule; +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) + { + addParam(jo, QStringLiteral("canonical_alias"), + pod.canonicalAlias); + addParam(jo, QStringLiteral("name"), pod.name); + addParam<>(jo, QStringLiteral("num_joined_members"), + pod.numJoinedMembers); + addParam<>(jo, QStringLiteral("room_id"), pod.roomId); + addParam(jo, QStringLiteral("topic"), pod.topic); + addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); + addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); + addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); + addParam(jo, QStringLiteral("join_rule"), pod.joinRule); + } + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) + { + fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); + fromJson(jo.value("name"_ls), pod.name); + fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); + fromJson(jo.value("room_id"_ls), pod.roomId); + fromJson(jo.value("topic"_ls), pod.topic); + fromJson(jo.value("world_readable"_ls), pod.worldReadable); + fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); + fromJson(jo.value("join_rule"_ls), pod.joinRule); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 2938b4ec..ca512280 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -6,81 +6,13 @@ #include "converters.h" -namespace Quotient { - -struct PublicRoomsChunk { - /// Aliases of the room. May be empty. - QStringList aliases; - - /// The canonical alias of the room, if any. - QString canonicalAlias; - - /// The name of the room, if any. - QString name; - - /// The number of members joined to the room. - int numJoinedMembers; - - /// The ID of the room. - QString roomId; - - /// The topic of the room, if any. - QString topic; - - /// Whether the room may be viewed by guest users without joining. - bool worldReadable; - - /// Whether guest users may join the room and participate in it. - /// If they can, they will be subject to ordinary power level - /// rules like any other user. - bool guestCanJoin; - - /// The URL for the room's avatar, if one is set. - QUrl avatarUrl; - - /// The room's join rule. When not present, the room is assumed to - /// be `public`. Note that rooms with `invite` join rules are not - /// expected here, but rooms with `knock` rules are given their - /// near-public nature. - QString joinRule; -}; - -template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) - { - addParam(jo, QStringLiteral("aliases"), pod.aliases); - addParam(jo, QStringLiteral("canonical_alias"), - pod.canonicalAlias); - addParam(jo, QStringLiteral("name"), pod.name); - addParam<>(jo, QStringLiteral("num_joined_members"), - pod.numJoinedMembers); - addParam<>(jo, QStringLiteral("room_id"), pod.roomId); - addParam(jo, QStringLiteral("topic"), pod.topic); - addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); - addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); - addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); - addParam(jo, QStringLiteral("join_rule"), pod.joinRule); - } - static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) - { - fromJson(jo.value("aliases"_ls), pod.aliases); - fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); - fromJson(jo.value("name"_ls), pod.name); - fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); - fromJson(jo.value("room_id"_ls), pod.roomId); - fromJson(jo.value("topic"_ls), pod.topic); - fromJson(jo.value("world_readable"_ls), pod.worldReadable); - fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); - fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); - fromJson(jo.value("join_rule"_ls), pod.joinRule); - } -}; +#include "csapi/definitions/public_rooms_chunk.h" +namespace Quotient { /// A list of the rooms on the server. struct PublicRoomsResponse { /// A paginated chunk of public rooms. - QVector chunk; + QVector chunk; /// A pagination token for the response. The absence of this token /// means there are no more results to fetch and the client should diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index ce66d075..6a048ba8 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -24,9 +24,7 @@ struct PushCondition { QString key; /// Required for `event_match` conditions. The glob-style pattern to - /// match against. Patterns with no special glob characters should be - /// treated as having asterisks prepended and appended when testing the - /// condition. + /// match against. QString pattern; /// Required for `room_member_count` conditions. A decimal integer diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index da6dbc76..fb58633c 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -9,30 +9,30 @@ using namespace Quotient; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/devices")); + makePath("/_matrix/client/v3", "/devices")); } GetDevicesJob::GetDevicesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), - makePath("/_matrix/client/r0", "/devices")) + makePath("/_matrix/client/v3", "/devices")) {} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/devices/", + makePath("/_matrix/client/v3", "/devices/", deviceId)); } GetDeviceJob::GetDeviceJob(const QString& deviceId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), - makePath("/_matrix/client/r0", "/devices/", deviceId)) + makePath("/_matrix/client/v3", "/devices/", deviceId)) {} UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), - makePath("/_matrix/client/r0", "/devices/", deviceId)) + makePath("/_matrix/client/v3", "/devices/", deviceId)) { QJsonObject _data; addParam(_data, QStringLiteral("display_name"), displayName); @@ -42,7 +42,7 @@ UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable& auth) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), - makePath("/_matrix/client/r0", "/devices/", deviceId)) + makePath("/_matrix/client/v3", "/devices/", deviceId)) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); @@ -52,7 +52,7 @@ DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), - makePath("/_matrix/client/r0", "/delete_devices")) + makePath("/_matrix/client/v3", "/delete_devices")) { QJsonObject _data; addParam<>(_data, QStringLiteral("devices"), devices); diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index 430d2132..c10389b3 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -86,7 +86,8 @@ public: * This API endpoint uses the [User-Interactive Authentication * API](/client-server-api/#user-interactive-authentication-api). * - * Deletes the given device, and invalidates any access token associated with it. + * Deletes the given device, and invalidates any access token associated with + * it. */ class QUOTIENT_API DeleteDeviceJob : public BaseJob { public: diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index b351b4ef..86b14f3a 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -8,7 +8,7 @@ using namespace Quotient; SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), - makePath("/_matrix/client/r0", "/directory/room/", roomAlias)) + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)) { QJsonObject _data; addParam<>(_data, QStringLiteral("room_id"), roomId); @@ -18,38 +18,38 @@ SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)); } GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), - makePath("/_matrix/client/r0", "/directory/room/", roomAlias), + makePath("/_matrix/client/v3", "/directory/room/", roomAlias), false) {} QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)); } DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), - makePath("/_matrix/client/r0", "/directory/room/", roomAlias)) + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)) {} QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases")); } GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases")) { addExpectedKey("aliases"); } diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 877838e2..4ebbbf98 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -20,7 +20,7 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& filter) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/context/", eventId), queryToGetEventContext(limit, filter)); } @@ -30,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId, Omittable limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/context/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/context/", eventId), queryToGetEventContext(limit, filter)) {} diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 38c68be7..57cb1271 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -8,7 +8,7 @@ using namespace Quotient; DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/filter")) + makePath("/_matrix/client/v3", "/user/", userId, "/filter")) { setRequestData(RequestData(toJson(filter))); addExpectedKey("filter_id"); @@ -18,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/filter/", filterId)); } GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) : BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/filter/", + makePath("/_matrix/client/v3", "/user/", userId, "/filter/", filterId)) {} diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 39d24611..bc1863ce 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -9,7 +9,7 @@ using namespace Quotient; InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index 21e6cb74..cb9d052b 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -14,7 +14,7 @@ namespace Quotient { * This version of the API requires that the inviter knows the Matrix * identifier of the invitee. The other is documented in the* * [third party invites - * section](/client-server-api/#post_matrixclientr0roomsroomidinvite-1). + * section](/client-server-api/#post_matrixclientv3roomsroomidinvite-1). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 373c1c6a..b05bd964 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -10,7 +10,7 @@ JoinRoomByIdJob::JoinRoomByIdJob( const QString& roomId, const Omittable& thirdPartySigned, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/join")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/join")) { QJsonObject _data; addParam(_data, QStringLiteral("third_party_signed"), @@ -32,7 +32,7 @@ JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const Omittable& thirdPartySigned, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), - makePath("/_matrix/client/r0", "/join/", roomIdOrAlias), + makePath("/_matrix/client/v3", "/join/", roomIdOrAlias), queryToJoinRoom(serverName)) { QJsonObject _data; diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index f64152f7..233537bb 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -22,8 +22,8 @@ namespace Quotient { * * After a user has joined a room, the room will appear as an entry in the * response of the - * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and - * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. + * [`/initialSync`](/client-server-api/#get_matrixclientv3initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs. */ class QUOTIENT_API JoinRoomByIdJob : public BaseJob { public: @@ -64,8 +64,8 @@ public: * * After a user has joined a room, the room will appear as an entry in the * response of the - * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and - * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. + * [`/initialSync`](/client-server-api/#get_matrixclientv3initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs. */ class QUOTIENT_API JoinRoomJob : public BaseJob { public: diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index d6bd2fab..d4996664 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -7,13 +7,15 @@ using namespace Quotient; UploadKeysJob::UploadKeysJob(const Omittable& deviceKeys, - const QHash& oneTimeKeys) + const OneTimeKeys& oneTimeKeys, + const OneTimeKeys& fallbackKeys) : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), - makePath("/_matrix/client/r0", "/keys/upload")) + makePath("/_matrix/client/v3", "/keys/upload")) { QJsonObject _data; addParam(_data, QStringLiteral("device_keys"), deviceKeys); addParam(_data, QStringLiteral("one_time_keys"), oneTimeKeys); + addParam(_data, QStringLiteral("fallback_keys"), fallbackKeys); setRequestData(std::move(_data)); addExpectedKey("one_time_key_counts"); } @@ -21,7 +23,7 @@ UploadKeysJob::UploadKeysJob(const Omittable& deviceKeys, QueryKeysJob::QueryKeysJob(const QHash& deviceKeys, Omittable timeout, const QString& token) : BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"), - makePath("/_matrix/client/r0", "/keys/query")) + makePath("/_matrix/client/v3", "/keys/query")) { QJsonObject _data; addParam(_data, QStringLiteral("timeout"), timeout); @@ -34,7 +36,7 @@ ClaimKeysJob::ClaimKeysJob( const QHash>& oneTimeKeys, Omittable timeout) : BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"), - makePath("/_matrix/client/r0", "/keys/claim")) + makePath("/_matrix/client/v3", "/keys/claim")) { QJsonObject _data; addParam(_data, QStringLiteral("timeout"), timeout); @@ -55,13 +57,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/keys/changes"), queryToGetKeysChanges(from, to)); } GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) : BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"), - makePath("/_matrix/client/r0", "/keys/changes"), + makePath("/_matrix/client/v3", "/keys/changes"), queryToGetKeysChanges(from, to)) {} diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index ce1ca9ed..2f2ebc6d 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,6 +4,8 @@ #pragma once +#include "e2ee/e2ee.h" + #include "csapi/definitions/cross_signing_key.h" #include "csapi/definitions/device_keys.h" @@ -30,14 +32,32 @@ public: * by the [key algorithm](/client-server-api/#key-algorithms). * * May be absent if no new one-time keys are required. + * + * \param fallbackKeys + * The public key which should be used if the device's one-time keys + * are exhausted. The fallback key is not deleted once used, but should + * be replaced when additional one-time keys are being uploaded. The + * server will notify the client of the fallback key being used through + * `/sync`. + * + * There can only be at most one key per algorithm uploaded, and the + * server will only persist one key per algorithm. + * + * When uploading a signed key, an additional `fallback: true` key should + * be included to denote that the key is a fallback key. + * + * May be absent if a new fallback key is not required. */ explicit UploadKeysJob(const Omittable& deviceKeys = none, - const QHash& oneTimeKeys = {}); + const OneTimeKeys& oneTimeKeys = {}, + const OneTimeKeys& fallbackKeys = {}); // Result properties /// For each key algorithm, the number of unclaimed one-time keys /// of that type currently held on the server for this device. + /// If an algorithm is not listed, the count for that algorithm + /// is to be assumed zero. QHash oneTimeKeyCounts() const { return loadFromJson>("one_time_key_counts"_ls); @@ -207,9 +227,12 @@ public: /// /// See the [key algorithms](/client-server-api/#key-algorithms) section for /// information on the Key Object format. - QHash> oneTimeKeys() const + /// + /// If necessary, the claimed key might be a fallback key. Fallback + /// keys are re-used by the server until replaced by the device. + QHash> oneTimeKeys() const { - return loadFromJson>>( + return loadFromJson>>( "one_time_keys"_ls); } }; @@ -233,7 +256,7 @@ public: * \param from * The desired start point of the list. Should be the `next_batch` field * from a response to an earlier call to - * [`/sync`](/client-server-api/#get_matrixclientr0sync). Users who have not + * [`/sync`](/client-server-api/#get_matrixclientv3sync). Users who have not * uploaded new device identity keys since this point, nor deleted * existing devices with identity keys since then, will be excluded * from the results. @@ -241,7 +264,7 @@ public: * \param to * The desired end point of the list. Should be the `next_batch` * field from a recent call to - * [`/sync`](/client-server-api/#get_matrixclientr0sync) - typically the + * [`/sync`](/client-server-api/#get_matrixclientv3sync) - typically the * most recent such call. This may be used by the server as a hint to check * its caches are up to date. */ diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 433e592c..3bedcb34 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -9,7 +9,7 @@ using namespace Quotient; KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/kick")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/kick")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp index 73e13e6e..ba541643 100644 --- a/lib/csapi/knocking.cpp +++ b/lib/csapi/knocking.cpp @@ -16,7 +16,7 @@ auto queryToKnockRoom(const QStringList& serverName) KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"), - makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias), + makePath("/_matrix/client/v3", "/knock/", roomIdOrAlias), queryToKnockRoom(serverName)) { QJsonObject _data; diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h index e3645b59..f43033a8 100644 --- a/lib/csapi/knocking.h +++ b/lib/csapi/knocking.h @@ -25,7 +25,7 @@ namespace Quotient { * history visibility to the user. * * The knock will appear as an entry in the response of the - * [`/sync`](/client-server-api/#get_matrixclientr0sync) API. + * [`/sync`](/client-server-api/#get_matrixclientv3sync) API. */ class QUOTIENT_API KnockRoomJob : public BaseJob { public: diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index 0e5386be..84340b94 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -8,7 +8,7 @@ using namespace Quotient; LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/leave")) { QJsonObject _data; addParam(_data, QStringLiteral("reason"), reason); @@ -18,11 +18,11 @@ LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget")); } ForgetRoomJob::ForgetRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget")) {} diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index 22ba04da..cdcf3eb2 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/joined_rooms")); } GetJoinedRoomsJob::GetJoinedRoomsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), - makePath("/_matrix/client/r0", "/joined_rooms")) + makePath("/_matrix/client/v3", "/joined_rooms")) { addExpectedKey("joined_rooms"); } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 25f8da5c..417e50b3 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -10,21 +10,21 @@ QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/directory/list/room/", roomId)); } GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"), - makePath("/_matrix/client/r0", "/directory/list/room/", roomId), + makePath("/_matrix/client/v3", "/directory/list/room/", roomId), false) {} SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"), - makePath("/_matrix/client/r0", "/directory/list/room/", roomId)) + makePath("/_matrix/client/v3", "/directory/list/room/", roomId)) { QJsonObject _data; addParam(_data, QStringLiteral("visibility"), visibility); @@ -46,7 +46,7 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable limit, const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/publicRooms"), queryToGetPublicRooms(limit, since, server)); } @@ -54,7 +54,7 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable limit, GetPublicRoomsJob::GetPublicRoomsJob(Omittable limit, const QString& since, const QString& server) : BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"), - makePath("/_matrix/client/r0", "/publicRooms"), + makePath("/_matrix/client/v3", "/publicRooms"), queryToGetPublicRooms(limit, since, server), {}, false) { addExpectedKey("chunk"); @@ -74,7 +74,7 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), - makePath("/_matrix/client/r0", "/publicRooms"), + makePath("/_matrix/client/v3", "/publicRooms"), queryToQueryPublicRooms(server)) { QJsonObject _data; diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index e1f03db7..701a74f6 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -4,7 +4,7 @@ #pragma once -#include "csapi/definitions/public_rooms_response.h" +#include "csapi/definitions/public_rooms_chunk.h" #include "jobs/basejob.h" @@ -103,9 +103,9 @@ public: // Result properties /// A paginated chunk of public rooms. - QVector chunk() const + QVector chunk() const { - return loadFromJson>("chunk"_ls); + return loadFromJson>("chunk"_ls); } /// A pagination token for the response. The absence of this token @@ -182,9 +182,9 @@ public: // Result properties /// A paginated chunk of public rooms. - QVector chunk() const + QVector chunk() const { - return loadFromJson>("chunk"_ls); + return loadFromJson>("chunk"_ls); } /// A pagination token for the response. The absence of this token diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 71fd93c5..b2175a05 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/login")); + makePath("/_matrix/client/v3", "/login")); } GetLoginFlowsJob::GetLoginFlowsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), - makePath("/_matrix/client/r0", "/login"), false) + makePath("/_matrix/client/v3", "/login"), false) {} LoginJob::LoginJob(const QString& type, @@ -23,7 +23,7 @@ LoginJob::LoginJob(const QString& type, const QString& deviceId, const QString& initialDeviceDisplayName) : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), - makePath("/_matrix/client/r0", "/login"), false) + makePath("/_matrix/client/v3", "/login"), false) { QJsonObject _data; addParam<>(_data, QStringLiteral("type"), type); diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp index e8083e31..9ec54c71 100644 --- a/lib/csapi/logout.cpp +++ b/lib/csapi/logout.cpp @@ -9,21 +9,21 @@ using namespace Quotient; QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/logout")); + makePath("/_matrix/client/v3", "/logout")); } LogoutJob::LogoutJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), - makePath("/_matrix/client/r0", "/logout")) + makePath("/_matrix/client/v3", "/logout")) {} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/logout/all")); } LogoutAllJob::LogoutAllJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), - makePath("/_matrix/client/r0", "/logout/all")) + makePath("/_matrix/client/v3", "/logout/all")) {} diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 1a93b75b..0b2c99ce 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -11,7 +11,7 @@ auto queryToGetRoomEvents(const QString& from, const QString& to, const QString& filter) { QUrlQuery _q; - addParam<>(_q, QStringLiteral("from"), from); + addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); addParam(_q, QStringLiteral("limit"), limit); @@ -20,20 +20,23 @@ auto queryToGetRoomEvents(const QString& from, const QString& to, } QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, - const QString& from, const QString& dir, + const QString& dir, const QString& from, const QString& to, Omittable limit, const QString& filter) { return BaseJob::makeRequestUrl( std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)); } -GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, - const QString& dir, const QString& to, +GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& dir, + const QString& from, const QString& to, Omittable limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)) -{} +{ + addExpectedKey("start"); + addExpectedKey("chunk"); +} diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 8c18f104..9831ae2d 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -25,20 +25,30 @@ public: * \param roomId * The room to get events from. * + * \param dir + * The direction to return events from. If this is set to `f`, events + * will be returned in chronological order starting at `from`. If it + * is set to `b`, events will be returned in *reverse* chronological + * order, again starting at `from`. + * * \param from * The token to start returning events from. This token can be obtained - * from a `prev_batch` token returned for each room by the sync API, - * or from a `start` or `end` token returned by a previous request - * to this endpoint. + * from a `prev_batch` or `next_batch` token returned by the `/sync` + * endpoint, or from an `end` token returned by a previous request to this + * endpoint. * - * \param dir - * The direction to return events from. + * This endpoint can also accept a value returned as a `start` token + * by a previous request to this endpoint, though servers are not + * required to support this. Clients should not rely on the behaviour. + * + * If it is not provided, the homeserver shall return a list of messages + * from the first or last (per the value of the `dir` parameter) visible + * event in the room history for the requesting user. * * \param to * The token to stop returning events at. This token can be obtained from - * a `prev_batch` token returned for each room by the sync endpoint, - * or from a `start` or `end` token returned by a previous request to - * this endpoint. + * a `prev_batch` or `next_batch` token returned by the `/sync` endpoint, + * or from an `end` token returned by a previous request to this endpoint. * * \param limit * The maximum number of events to return. Default: 10. @@ -46,8 +56,8 @@ public: * \param filter * A JSON RoomEventFilter to filter returned events with. */ - explicit GetRoomEventsJob(const QString& roomId, const QString& from, - const QString& dir, const QString& to = {}, + explicit GetRoomEventsJob(const QString& roomId, const QString& dir, + const QString& from = {}, const QString& to = {}, Omittable limit = none, const QString& filter = {}); @@ -57,25 +67,34 @@ public: * is necessary but the job itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, - const QString& from, const QString& dir, + const QString& dir, const QString& from = {}, const QString& to = {}, Omittable limit = none, const QString& filter = {}); // Result properties - /// The token the pagination starts from. If `dir=b` this will be - /// the token supplied in `from`. + /// A token corresponding to the start of `chunk`. This will be the same as + /// the value given in `from`. QString begin() const { return loadFromJson("start"_ls); } - /// The token the pagination ends at. If `dir=b` this token should - /// be used again to request even earlier events. + /// A token corresponding to the end of `chunk`. This token can be passed + /// back to this endpoint to request further events. + /// + /// If no further events are available (either because we have + /// reached the start of the timeline, or because the user does + /// not have permission to see any more events), this property + /// is omitted from the response. QString end() const { return loadFromJson("end"_ls); } /// A list of room events. The order depends on the `dir` parameter. /// For `dir=b` events will be in reverse-chronological order, - /// for `dir=f` in chronological order, so that events start - /// at the `from` point. + /// for `dir=f` in chronological order. (The exact definition of + /// `chronological` is dependent on the server implementation.) + /// + /// Note that an empty `chunk` does not *necessarily* imply that no more + /// events are available. Clients should continue to paginate until no `end` + /// property is returned. RoomEvents chunk() { return takeFromJson("chunk"_ls); } /// A list of state events relevant to showing the `chunk`. For example, if @@ -86,7 +105,7 @@ public: /// may remove membership events which would have already been /// sent to the client in prior calls to this endpoint, assuming /// the membership of those members has not changed. - StateEvents state() { return takeFromJson("state"_ls); } + RoomEvents state() { return takeFromJson("state"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index 1e523c6f..38aed174 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -21,7 +21,7 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/notifications"), queryToGetNotifications(from, limit, only)); } @@ -30,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable limit, const QString& only) : BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"), - makePath("/_matrix/client/r0", "/notifications"), + makePath("/_matrix/client/v3", "/notifications"), queryToGetNotifications(from, limit, only)) { addExpectedKey("notifications"); diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 23211758..48167877 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -43,7 +43,8 @@ public: /*! \brief Gets a list of events that the user has been notified about * * \param from - * Pagination token given to retrieve the next set of events. + * Pagination token to continue from. This should be the `next_token` + * returned from an earlier call to this endpoint. * * \param limit * Limit on the number of events to return in this request. diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 5c93a2d7..8349e6db 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -9,7 +9,7 @@ using namespace Quotient; RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"), - makePath("/_matrix/client/r0", "/user/", userId, + makePath("/_matrix/client/v3", "/user/", userId, "/openid/request_token")) { setRequestData(RequestData(toJson(body))); diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 773b6011..b3f72a25 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -43,7 +43,10 @@ public: /// Specification](http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse) /// with the only difference being the lack of an `id_token`. Instead, /// the Matrix homeserver's name is provided. - OpenidToken tokenData() const { return fromJson(jsonData()); } + OpenIdCredentials tokenData() const + { + return fromJson(jsonData()); + } }; } // namespace Quotient diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index eb5d22fa..9dd1445e 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -20,13 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/events"), + makePath("/_matrix/client/v3", "/events"), queryToPeekEvents(from, timeout, roomId)); } PeekEventsJob::PeekEventsJob(const QString& from, Omittable timeout, const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("PeekEventsJob"), - makePath("/_matrix/client/r0", "/events"), + makePath("/_matrix/client/v3", "/events"), queryToPeekEvents(from, timeout, roomId)) {} diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 14cb6f0b..ff688c49 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -9,7 +9,7 @@ namespace Quotient { -/*! \brief Listen on the event stream. +/*! \brief Listen on the event stream of a particular room. * * This will listen for new events related to a particular room and return * them to the caller. This will block until an event is received, or until @@ -24,7 +24,7 @@ namespace Quotient { */ class QUOTIENT_API PeekEventsJob : public BaseJob { public: - /*! \brief Listen on the event stream. + /*! \brief Listen on the event stream of a particular room. * * \param from * The token to stream from. This token is either from a previous diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 4f77c466..6d154ebd 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -9,7 +9,7 @@ using namespace Quotient; SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), - makePath("/_matrix/client/r0", "/presence/", userId, "/status")) + makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { QJsonObject _data; addParam<>(_data, QStringLiteral("presence"), presence); @@ -20,13 +20,13 @@ SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/presence/", + makePath("/_matrix/client/v3", "/presence/", userId, "/status")); } GetPresenceJob::GetPresenceJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), - makePath("/_matrix/client/r0", "/presence/", userId, "/status")) + makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { addExpectedKey("presence"); } diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 64ac84ca..7621d828 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -9,7 +9,8 @@ using namespace Quotient; SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) : BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"), - makePath("/_matrix/client/r0", "/profile/", userId, "/displayname")) + makePath("/_matrix/client/v3", "/profile/", userId, + "/displayname")) { QJsonObject _data; addParam<>(_data, QStringLiteral("displayname"), displayname); @@ -19,19 +20,20 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId, QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/profile/", + makePath("/_matrix/client/v3", "/profile/", userId, "/displayname")); } GetDisplayNameJob::GetDisplayNameJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), - makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"), + makePath("/_matrix/client/v3", "/profile/", userId, + "/displayname"), false) {} SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), - makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url")) + makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url")) { QJsonObject _data; addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); @@ -41,24 +43,24 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/profile/", + makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url")); } GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"), - makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"), + makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url"), false) {} QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/profile/", + makePath("/_matrix/client/v3", "/profile/", userId)); } GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), - makePath("/_matrix/client/r0", "/profile/", userId), false) + makePath("/_matrix/client/v3", "/profile/", userId), false) {} diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index ef4b3767..498be3ee 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushers")); + makePath("/_matrix/client/v3", "/pushers")); } GetPushersJob::GetPushersJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), - makePath("/_matrix/client/r0", "/pushers")) + makePath("/_matrix/client/v3", "/pushers")) {} PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, @@ -23,7 +23,7 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& lang, const PusherData& data, const QString& profileTag, Omittable append) : BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"), - makePath("/_matrix/client/r0", "/pushers/set")) + makePath("/_matrix/client/v3", "/pushers/set")) { QJsonObject _data; addParam<>(_data, QStringLiteral("pushkey"), pushkey); diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index 0d840788..6b0effd4 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/pushrules")); } GetPushRulesJob::GetPushRulesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), - makePath("/_matrix/client/r0", "/pushrules")) + makePath("/_matrix/client/v3", "/pushrules")) { addExpectedKey("global"); } @@ -23,14 +23,14 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)); } GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)) {} @@ -39,14 +39,14 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)); } DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)) {} @@ -65,7 +65,7 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QVector& conditions, const QString& pattern) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId), queryToSetPushRule(before, after)) { @@ -81,7 +81,7 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/enabled")); } @@ -90,7 +90,7 @@ IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/enabled")) { addExpectedKey("enabled"); @@ -100,7 +100,7 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/enabled")) { QJsonObject _data; @@ -113,7 +113,7 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/actions")); } @@ -122,7 +122,7 @@ GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/actions")) { addExpectedKey("actions"); @@ -133,7 +133,7 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& ruleId, const QVector& actions) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/actions")) { QJsonObject _data; diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index f2edb71e..dc84f887 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -10,7 +10,7 @@ SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/read_markers")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/read_markers")) { QJsonObject _data; addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 401c3bfe..8feab986 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -10,7 +10,7 @@ PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType const QString& eventId, const QJsonObject& receipt) : BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/receipt/", receiptType, "/", eventId)) { setRequestData(RequestData(toJson(receipt))); diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index acf1b0e4..d67cb37b 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -9,7 +9,7 @@ using namespace Quotient; RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) : BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/redact/", eventId, "/", txnId)) { QJsonObject _data; diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 153abcee..aa4b4fad 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -20,7 +20,7 @@ RegisterJob::RegisterJob(const QString& kind, const QString& initialDeviceDisplayName, Omittable inhibitLogin) : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), - makePath("/_matrix/client/r0", "/register"), + makePath("/_matrix/client/v3", "/register"), queryToRegister(kind), {}, false) { QJsonObject _data; @@ -38,7 +38,7 @@ RegisterJob::RegisterJob(const QString& kind, RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"), - makePath("/_matrix/client/r0", "/register/email/requestToken"), + makePath("/_matrix/client/v3", "/register/email/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -47,7 +47,7 @@ RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), - makePath("/_matrix/client/r0", "/register/msisdn/requestToken"), + makePath("/_matrix/client/v3", "/register/msisdn/requestToken"), false) { setRequestData(RequestData(toJson(body))); @@ -57,7 +57,7 @@ ChangePasswordJob::ChangePasswordJob(const QString& newPassword, bool logoutDevices, const Omittable& auth) : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), - makePath("/_matrix/client/r0", "/account/password")) + makePath("/_matrix/client/v3", "/account/password")) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_password"), newPassword); @@ -70,7 +70,7 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordEmailJob"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/password/email/requestToken"), false) { @@ -81,7 +81,7 @@ RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/password/msisdn/requestToken"), false) { @@ -91,7 +91,7 @@ RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( DeactivateAccountJob::DeactivateAccountJob( const Omittable& auth, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), - makePath("/_matrix/client/r0", "/account/deactivate")) + makePath("/_matrix/client/v3", "/account/deactivate")) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); @@ -111,13 +111,14 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/register/available"), queryToCheckUsernameAvailability(username)); } -CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) +CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob( + const QString& username) : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), - makePath("/_matrix/client/r0", "/register/available"), + makePath("/_matrix/client/v3", "/register/available"), queryToCheckUsernameAvailability(username), {}, false) {} diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 10375971..39840008 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -227,7 +227,8 @@ public: * should be revoked if the request succeeds. * * When `false`, the server can still take advantage of the [soft logout - * method](/client-server-api/#soft-logout) for the user's remaining devices. + * method](/client-server-api/#soft-logout) for the user's remaining + * devices. * * \param auth * Additional authentication information for the user-interactive @@ -247,7 +248,7 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * endpoint, except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given email address could be found. The server may instead send an @@ -269,7 +270,7 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * endpoint, except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given email address could be found. The server may instead send an @@ -299,7 +300,7 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * endpoint, except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given phone number could be found. The server may instead send the SMS @@ -321,15 +322,16 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * endpoint, except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given phone number could be found. The server may instead send the SMS * to the given phone number prompting the user to create an account. * `M_THREEPID_IN_USE` may not be returned. * - * The homeserver should validate the phone number itself, either by sending - * a validation message itself or by using a service it has control over. + * The homeserver should validate the phone number itself, either by + * sending a validation message itself or by using a service it has control + * over. */ explicit RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body); @@ -377,8 +379,9 @@ public: * it must return an `id_server_unbind_result` of * `no-support`. */ - explicit DeactivateAccountJob(const Omittable& auth = none, - const QString& idServer = {}); + explicit DeactivateAccountJob( + const Omittable& auth = none, + const QString& idServer = {}); // Result properties diff --git a/lib/csapi/registration_tokens.cpp b/lib/csapi/registration_tokens.cpp new file mode 100644 index 00000000..9c1f0587 --- /dev/null +++ b/lib/csapi/registration_tokens.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "registration_tokens.h" + +using namespace Quotient; + +auto queryToRegistrationTokenValidity(const QString& token) +{ + QUrlQuery _q; + addParam<>(_q, QStringLiteral("token"), token); + return _q; +} + +QUrl RegistrationTokenValidityJob::makeRequestUrl(QUrl baseUrl, + const QString& token) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", + "/register/m.login.registration_token/validity"), + queryToRegistrationTokenValidity(token)); +} + +RegistrationTokenValidityJob::RegistrationTokenValidityJob(const QString& token) + : BaseJob(HttpVerb::Get, QStringLiteral("RegistrationTokenValidityJob"), + makePath("/_matrix/client/v1", + "/register/m.login.registration_token/validity"), + queryToRegistrationTokenValidity(token), {}, false) +{ + addExpectedKey("valid"); +} diff --git a/lib/csapi/registration_tokens.h b/lib/csapi/registration_tokens.h new file mode 100644 index 00000000..e3008dd4 --- /dev/null +++ b/lib/csapi/registration_tokens.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Query if a given registration token is still valid. + * + * Queries the server to determine if a given registration token is still + * valid at the time of request. This is a point-in-time check where the + * token might still expire by the time it is used. + * + * Servers should be sure to rate limit this endpoint to avoid brute force + * attacks. + */ +class QUOTIENT_API RegistrationTokenValidityJob : public BaseJob { +public: + /*! \brief Query if a given registration token is still valid. + * + * \param token + * The token to check validity of. + */ + explicit RegistrationTokenValidityJob(const QString& token); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RegistrationTokenValidityJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& token); + + // Result properties + + /// True if the token is still valid, false otherwise. This should + /// additionally be false if the token is not a recognised token by + /// the server. + bool valid() const { return loadFromJson("valid"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index 0a76d5b8..b8e9a8d1 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -9,7 +9,7 @@ using namespace Quotient; ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, Omittable score, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/report/", eventId)) { QJsonObject _data; diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index f80f9300..93ab04d2 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -9,7 +9,7 @@ using namespace Quotient; SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/send/", eventType, "/", txnId)) { setRequestData(RequestData(toJson(body))); diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index fea3d59d..fcb6b24f 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -16,7 +16,8 @@ namespace Quotient { * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See - * [Room Events](/client-server-api/#room-events) for the m. event specification. + * [Room Events](/client-server-api/#room-events) for the m. event + * specification. */ class QUOTIENT_API SendMessageJob : public BaseJob { public: diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index f6d2e6ec..2253863a 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -11,7 +11,7 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& stateKey, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)) { setRequestData(RequestData(toJson(body))); diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index d4129cfb..3f67234d 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -8,7 +8,7 @@ using namespace Quotient; UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/upgrade")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/upgrade")) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_version"), newVersion); diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 5310aa32..563f4fa5 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -10,14 +10,14 @@ QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/event/", eventId)); } GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) : BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/event/", eventId)) {} @@ -26,7 +26,7 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)); } @@ -35,20 +35,20 @@ GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)) {} QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state")); } GetRoomStateJob::GetRoomStateJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/state")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state")) {} auto queryToGetMembersByRoom(const QString& at, const QString& membership, @@ -68,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, { return BaseJob::makeRequestUrl( std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)); } @@ -77,7 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)) {} @@ -85,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/joined_members")); } GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, + makePath("/_matrix/client/v3", "/rooms/", roomId, "/joined_members")) {} diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index f0815109..247fb13f 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -5,7 +5,6 @@ #pragma once #include "events/eventloader.h" -#include "events/roommemberevent.h" #include "jobs/basejob.h" namespace Quotient { @@ -38,7 +37,7 @@ public: // Result properties /// The full event. - EventPtr event() { return fromJson(jsonData()); } + RoomEventPtr event() { return fromJson(jsonData()); } }; /*! \brief Get the state identified by the type and key. @@ -146,10 +145,7 @@ public: // Result properties /// Get the list of members for this room. - EventsArray chunk() - { - return takeFromJson>("chunk"_ls); - } + StateEvents chunk() { return takeFromJson("chunk"_ls); } }; /*! \brief Gets the list of currently joined users and their profile data. @@ -157,9 +153,8 @@ public: * This API returns a map of MXIDs to member info objects for members of the * room. The current user must be in the room for it to work, unless it is an * Application Service in which case any of the AS's users must be in the room. - * This API is primarily for Application Services and should be faster to - * respond than `/members` as it can be implemented more efficiently on the - * server. + * This API is primarily for Application Services and should be faster to respond + * than `/members` as it can be implemented more efficiently on the server. */ class QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob { public: diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 295dd1cc..92300351 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -16,7 +16,7 @@ auto queryToSearch(const QString& nextBatch) SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), - makePath("/_matrix/client/r0", "/search"), + makePath("/_matrix/client/v3", "/search"), queryToSearch(nextBatch)) { QJsonObject _data; diff --git a/lib/csapi/space_hierarchy.cpp b/lib/csapi/space_hierarchy.cpp new file mode 100644 index 00000000..7c17ce8a --- /dev/null +++ b/lib/csapi/space_hierarchy.cpp @@ -0,0 +1,43 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "space_hierarchy.h" + +using namespace Quotient; + +auto queryToGetSpaceHierarchy(Omittable suggestedOnly, + Omittable limit, + Omittable maxDepth, const QString& from) +{ + QUrlQuery _q; + addParam(_q, QStringLiteral("suggested_only"), suggestedOnly); + addParam(_q, QStringLiteral("limit"), limit); + addParam(_q, QStringLiteral("max_depth"), maxDepth); + addParam(_q, QStringLiteral("from"), from); + return _q; +} + +QUrl GetSpaceHierarchyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + Omittable suggestedOnly, + Omittable limit, + Omittable maxDepth, + const QString& from) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), + queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from)); +} + +GetSpaceHierarchyJob::GetSpaceHierarchyJob(const QString& roomId, + Omittable suggestedOnly, + Omittable limit, + Omittable maxDepth, + const QString& from) + : BaseJob(HttpVerb::Get, QStringLiteral("GetSpaceHierarchyJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), + queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from)) +{ + addExpectedKey("rooms"); +} diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h new file mode 100644 index 00000000..adffe2f8 --- /dev/null +++ b/lib/csapi/space_hierarchy.h @@ -0,0 +1,88 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/../../event-schemas/schema/core-event-schema/stripped_state.h" +#include "csapi/definitions/public_rooms_chunk.h" + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Retrieve a portion of a space tree. + * + * Paginates over the space tree in a depth-first manner to locate child rooms + * of a given space. + * + * Where a child room is unknown to the local server, federation is used to fill + * in the details. The servers listed in the `via` array should be contacted to + * attempt to fill in missing rooms. + * + * Only [`m.space.child`](#mspacechild) state events of the room are considered. + * Invalid child rooms and parent events are not covered by this endpoint. + */ +class QUOTIENT_API GetSpaceHierarchyJob : public BaseJob { +public: + /*! \brief Retrieve a portion of a space tree. + * + * \param roomId + * The room ID of the space to get a hierarchy for. + * + * \param suggestedOnly + * Optional (default `false`) flag to indicate whether or not the server + * should only consider suggested rooms. Suggested rooms are annotated in + * their [`m.space.child`](#mspacechild) event contents. + * + * \param limit + * Optional limit for the maximum number of rooms to include per response. + * Must be an integer greater than zero. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param maxDepth + * Optional limit for how far to go into the space. Must be a non-negative + * integer. + * + * When reached, no further child rooms will be returned. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param from + * A pagination token from a previous result. If specified, `max_depth` + * and `suggested_only` cannot be changed from the first request. + */ + explicit GetSpaceHierarchyJob(const QString& roomId, + Omittable suggestedOnly = none, + Omittable limit = none, + Omittable maxDepth = none, + const QString& from = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetSpaceHierarchyJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + Omittable suggestedOnly = none, + Omittable limit = none, + Omittable maxDepth = none, + const QString& from = {}); + + // Result properties + + /// The rooms for the current page, with the current filters. + QVector rooms() const + { + return loadFromJson>("rooms"_ls); + } + + /// A token to supply to `from` to keep paginating the responses. Not + /// present when there are no further results. + QString nextBatch() const { return loadFromJson("next_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index 871d6ff6..71f8147c 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -16,14 +16,14 @@ auto queryToRedirectToSSO(const QString& redirectUrl) QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl)); } RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"), - makePath("/_matrix/client/r0", "/login/sso/redirect"), + makePath("/_matrix/client/v3", "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl), {}, false) {} @@ -38,7 +38,7 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId), queryToRedirectToIdP(redirectUrl)); } @@ -46,6 +46,6 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), - makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId), + makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId), queryToRedirectToIdP(redirectUrl), {}, false) {} diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index f717de6e..21cb18ed 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -10,13 +10,13 @@ QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags")); } GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags")) {} @@ -24,7 +24,7 @@ SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable order, const QVariantHash& additionalProperties) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags/", tag)) { QJsonObject _data; @@ -37,7 +37,7 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags/", tag)); } @@ -45,6 +45,6 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags/", tag)) {} diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 4c930668..1e5870ce 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -9,26 +9,26 @@ using namespace Quotient; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/protocols")); } GetProtocolsJob::GetProtocolsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), - makePath("/_matrix/client/r0", "/thirdparty/protocols")) + makePath("/_matrix/client/v3", "/thirdparty/protocols")) {} QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, const QString& protocol) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/protocol/", protocol)); } GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), - makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol)) + makePath("/_matrix/client/v3", "/thirdparty/protocol/", protocol)) {} auto queryToQueryLocationByProtocol(const QString& searchFields) @@ -43,7 +43,7 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)); } @@ -51,7 +51,7 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, QueryLocationByProtocolJob::QueryLocationByProtocolJob( const QString& protocol, const QString& searchFields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"), - makePath("/_matrix/client/r0", "/thirdparty/location/", protocol), + makePath("/_matrix/client/v3", "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)) {} @@ -67,7 +67,7 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)); } @@ -75,7 +75,7 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"), - makePath("/_matrix/client/r0", "/thirdparty/user/", protocol), + makePath("/_matrix/client/v3", "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)) {} @@ -89,14 +89,14 @@ auto queryToQueryLocationByAlias(const QString& alias) QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/location"), queryToQueryLocationByAlias(alias)); } QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), - makePath("/_matrix/client/r0", "/thirdparty/location"), + makePath("/_matrix/client/v3", "/thirdparty/location"), queryToQueryLocationByAlias(alias)) {} @@ -110,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid) QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/user"), queryToQueryUserByID(userid)); } QueryUserByIDJob::QueryUserByIDJob(const QString& userid) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), - makePath("/_matrix/client/r0", "/thirdparty/user"), + makePath("/_matrix/client/v3", "/thirdparty/user"), queryToQueryUserByID(userid)) {} diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index 59275e41..2d6df77d 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -10,7 +10,7 @@ InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& idAccessToken, const QString& medium, const QString& address) : BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { QJsonObject _data; addParam<>(_data, QStringLiteral("id_server"), idServer); diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index 1edb969e..1129a9a8 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -16,7 +16,7 @@ namespace Quotient { * The homeserver uses an identity server to perform the mapping from * third party identifier to a Matrix identifier. The other is documented in * the* [joining rooms - * section](/client-server-api/#post_matrixclientr0roomsroomidinvite). + * section](/client-server-api/#post_matrixclientv3roomsroomidinvite). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 628e8314..48e943db 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -10,7 +10,7 @@ SendToDeviceJob::SendToDeviceJob( const QString& eventType, const QString& txnId, const QHash>& messages) : BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"), - makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/", + makePath("/_matrix/client/v3", "/sendToDevice/", eventType, "/", txnId)) { QJsonObject _data; diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index c9673118..1b4fe147 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -9,7 +9,7 @@ using namespace Quotient; SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable timeout) : BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/typing/", userId)) { QJsonObject _data; diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index 48b727f0..e0db6f70 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -9,7 +9,7 @@ using namespace Quotient; SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable limit) : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), - makePath("/_matrix/client/r0", "/user_directory/search")) + makePath("/_matrix/client/v3", "/user_directory/search")) { QJsonObject _data; addParam<>(_data, QStringLiteral("search_term"), searchTerm); diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 4445dbd2..9f799cb0 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -12,11 +12,9 @@ namespace Quotient { * * Gets the versions of the specification supported by the server. * - * Values will take the form `rX.Y.Z`. - * - * Only the latest `Z` value will be reported for each supported `X.Y` value. - * i.e. if the server implements `r0.0.0`, `r0.0.1`, and `r1.2.0`, it will - * report `r0.0.1` and `r1.2.0`. + * Values will take the form `vX.Y` or `rX.Y.Z` in historical cases. See + * [the Specification Versioning](../#specification-versions) for more + * information. * * The server may additionally advertise experimental features it supports * through `unstable_features`. These features should be namespaced and diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index c748ad94..1e1f2441 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -9,10 +9,10 @@ using namespace Quotient; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/voip/turnServer")); } GetTurnServerJob::GetTurnServerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), - makePath("/_matrix/client/r0", "/voip/turnServer")) + makePath("/_matrix/client/v3", "/voip/turnServer")) {} diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index ed8a9817..af0c5d31 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/account/whoami")); } GetTokenOwnerJob::GetTokenOwnerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), - makePath("/_matrix/client/r0", "/account/whoami")) + makePath("/_matrix/client/v3", "/account/whoami")) { addExpectedKey("user_id"); } diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index fba099f6..3451dbc3 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -41,6 +41,14 @@ public: /// of application services) then this field can be omitted. /// Otherwise this is required. QString deviceId() const { return loadFromJson("device_id"_ls); } + + /// When `true`, the user is a [Guest User](#guest-access). When + /// not present or `false`, the user is presumed to be a non-guest + /// user. + Omittable isGuest() const + { + return loadFromJson>("is_guest"_ls); + } }; } // namespace Quotient -- cgit v1.2.3 From 21ae4eca4c06e500ec04a52ad42772bf8e8e9b6f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 31 May 2022 18:58:27 +0200 Subject: Tweak QOlmAccount and data structures around This is mainly to plug the definition of a string-to-variant map for one-time keys (see https://spec.matrix.org/v1.2/client-server-api/#key-algorithms) into the CS API generated code (see the "shortcut OneTimeKeys" commit for gtad.yaml); but along with it came considerable streamlining of code in qolmaccount.cpp. Using std::variant to store that map also warranted converters.h to gain support for that type (even wider than toJson() that is already in dev - a non-trivial merge from dev is in order). --- autotests/testolmaccount.cpp | 18 +++----- lib/converters.h | 20 ++++++++ lib/e2ee/e2ee.h | 10 ++-- lib/e2ee/qolmaccount.cpp | 108 ++++++++++++++++--------------------------- lib/e2ee/qolmaccount.h | 9 ++-- 5 files changed, 73 insertions(+), 92 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index e31ff6d3..c85718dd 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -198,7 +198,7 @@ void TestOlmAccount::uploadIdentityKey() QVERIFY(idKeys.curve25519.size() > 10); - OneTimeKeys unused; + UnsignedOneTimeKeys unused; auto request = olmAccount->createUploadKeyRequest(unused); connect(request, &BaseJob::result, this, [request, conn] { QCOMPARE(request->oneTimeKeyCounts().size(), 0); @@ -221,7 +221,7 @@ void TestOlmAccount::uploadOneTimeKeys() auto oneTimeKeys = olmAccount->oneTimeKeys(); - QHash oneTimeKeysHash; + OneTimeKeys oneTimeKeysHash; const auto curve = oneTimeKeys.curve25519(); for (const auto &[keyId, key] : asKeyValueRange(curve)) { oneTimeKeysHash["curve25519:"+keyId] = key; @@ -247,12 +247,10 @@ void TestOlmAccount::uploadSignedOneTimeKeys() QCOMPARE(nKeys, 5); auto oneTimeKeys = olmAccount->oneTimeKeys(); - QHash oneTimeKeysHash; + OneTimeKeys oneTimeKeysHash; const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys); for (const auto &[keyId, key] : asKeyValueRange(signedKey)) { - QVariant var; - var.setValue(key); - oneTimeKeysHash[keyId] = var; + oneTimeKeysHash[keyId] = key; } auto request = new UploadKeysJob(none, oneTimeKeysHash); connect(request, &BaseJob::result, this, [request, nKeys, conn] { @@ -410,11 +408,9 @@ void TestOlmAccount::claimKeys() // The key is the one bob sent. const auto& oneTimeKey = job->oneTimeKeys().value(userId).value(deviceId); - QVERIFY(oneTimeKey.canConvert()); - - const auto varMap = oneTimeKey.toMap(); - QVERIFY(std::any_of(varMap.constKeyValueBegin(), - varMap.constKeyValueEnd(), [](const auto& kv) { + QVERIFY(std::any_of(oneTimeKey.constKeyValueBegin(), + oneTimeKey.constKeyValueEnd(), + [](const auto& kv) { return kv.first.startsWith( SignedCurve25519Key); })); diff --git a/lib/converters.h b/lib/converters.h index 5e3becb8..64a5cfb6 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -224,6 +224,26 @@ struct QUOTIENT_API JsonConverter { static QVariant load(const QJsonValue& jv); }; +template +inline QJsonValue toJson(const std::variant& v) +{ + // std::visit requires all overloads to return the same type - and + // QJsonValue is a perfect candidate for that same type (assuming that + // variants never occur on the top level in Matrix API) + return std::visit( + [](const auto& value) { return QJsonValue { toJson(value) }; }, v); +} + +template +struct QUOTIENT_API JsonConverter> { + static std::variant load(const QJsonValue& jv) + { + if (jv.isString()) + return fromJson(jv); + return fromJson(jv); + } +}; + template struct JsonConverter> { static QJsonValue dump(const Omittable& from) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 8e433d60..f97eb27a 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -70,15 +70,12 @@ struct IdentityKeys }; //! Struct representing the one-time keys. -struct QUOTIENT_API OneTimeKeys +struct QUOTIENT_API UnsignedOneTimeKeys { QHash> keys; //! Get the HashMap containing the curve25519 one-time keys. - QHash curve25519() const; - - //! Get a reference to the hashmap corresponding to given key type. -// std::optional> get(QString keyType) const; + QHash curve25519() const { return keys[Curve25519Key]; } }; //! Struct representing the signed one-time keys. @@ -93,7 +90,6 @@ public: QHash> signatures; }; - template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SignedOneTimeKey& result) @@ -109,6 +105,8 @@ struct JsonObjectConverter { } }; +using OneTimeKeys = QHash>; + template class asKeyValueRange { diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 72dddafb..4cfc6151 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -17,19 +17,6 @@ using namespace Quotient; -QHash OneTimeKeys::curve25519() const -{ - return keys[Curve25519Key]; -} - -//std::optional> OneTimeKeys::get(QString keyType) const -//{ -// if (!keys.contains(keyType)) { -// return std::nullopt; -// } -// return keys[keyType]; -//} - // Convert olm error to enum QOlmError lastError(OlmAccount *account) { return fromString(olm_account_last_error(account)); @@ -122,20 +109,15 @@ QByteArray QOlmAccount::sign(const QJsonObject &message) const QByteArray QOlmAccount::signIdentityKeys() const { const auto keys = identityKeys(); - QJsonObject body - { - {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, - {"user_id", m_userId}, - {"device_id", m_deviceId}, - {"keys", - QJsonObject{ - {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)}, - {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)} - } - } - }; - return sign(QJsonDocument(body).toJson(QJsonDocument::Compact)); - + return sign(QJsonObject { + { "algorithms", QJsonArray { "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" } }, + { "user_id", m_userId }, + { "device_id", m_deviceId }, + { "keys", QJsonObject { { QStringLiteral("curve25519:") + m_deviceId, + QString::fromUtf8(keys.curve25519) }, + { QStringLiteral("ed25519:") + m_deviceId, + QString::fromUtf8(keys.ed25519) } } } }); } size_t QOlmAccount::maxNumberOfOneTimeKeys() const @@ -145,9 +127,13 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) { - const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + const size_t randomLength = + olm_account_generate_one_time_keys_random_length(m_account, + numberOfKeys); QByteArray randomBuffer = getRandom(randomLength); - const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); + const auto error = + olm_account_generate_one_time_keys(m_account, numberOfKeys, + randomBuffer.data(), randomLength); if (error == olm_error()) { throw lastError(m_account); @@ -156,7 +142,7 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) return error; } -OneTimeKeys QOlmAccount::oneTimeKeys() const +UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const { const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); @@ -166,34 +152,25 @@ OneTimeKeys QOlmAccount::oneTimeKeys() const throw lastError(m_account); } const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); - OneTimeKeys oneTimeKeys; + UnsignedOneTimeKeys oneTimeKeys; fromJson(json, oneTimeKeys.keys); return oneTimeKeys; } -QHash QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const { - QHash signedOneTimeKeys; - for (const auto &keyid : keys.curve25519().keys()) { - const auto oneTimeKey = keys.curve25519()[keyid]; - QByteArray sign = signOneTimeKey(oneTimeKey); - signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); - } + OneTimeKeys signedOneTimeKeys; + const auto& curveKeys = keys.curve25519(); + for (const auto& [keyId, key] : asKeyValueRange(curveKeys)) + signedOneTimeKeys["signed_curve25519:" % keyId] = + signedOneTimeKey(key.toUtf8(), sign(QJsonObject{{"key", key}})); return signedOneTimeKeys; } -SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const +SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray& key, + const QString& signature) const { - SignedOneTimeKey sign{}; - sign.key = key; - sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; - return sign; -} - -QByteArray QOlmAccount::signOneTimeKey(const QString &key) const -{ - QJsonDocument j(QJsonObject{{"key", key}}); - return sign(j.toJson(QJsonDocument::Compact)); + return { key, { { m_userId, { { "ed25519:" + m_deviceId, signature } } } } }; } std::optional QOlmAccount::removeOneTimeKeys( @@ -227,39 +204,32 @@ DeviceKeys QOlmAccount::deviceKeys() const return deviceKeys; } -UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys) +UploadKeysJob* QOlmAccount::createUploadKeyRequest( + const UnsignedOneTimeKeys& oneTimeKeys) const { - auto keys = deviceKeys(); - - if (oneTimeKeys.curve25519().isEmpty()) { - return new UploadKeysJob(keys); - } - - // Sign & append the one time keys. - auto temp = signOneTimeKeys(oneTimeKeys); - QHash oneTimeKeysSigned; - for (const auto &[keyId, key] : asKeyValueRange(temp)) { - oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key)); - } - - return new UploadKeysJob(keys, oneTimeKeysSigned); + return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys)); } -QOlmExpected QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) +QOlmExpected QOlmAccount::createInboundSession( + const QOlmMessage& preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSession(this, preKeyMessage); } -QOlmExpected QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) +QOlmExpected QOlmAccount::createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); - return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, + preKeyMessage); } -QOlmExpected QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +QOlmExpected QOlmAccount::createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey) { - return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); + return QOlmSession::createOutboundSession(this, theirIdentityKey, + theirOneTimeKey); } void QOlmAccount::markKeysAsPublished() diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index ee2aa69d..23fe58dd 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -59,17 +59,14 @@ public: size_t generateOneTimeKeys(size_t numberOfKeys); //! Gets the OlmAccount's one time keys formatted as JSON. - OneTimeKeys oneTimeKeys() const; + UnsignedOneTimeKeys oneTimeKeys() const; //! Sign all one time keys. - QHash signOneTimeKeys(const OneTimeKeys &keys) const; - - //! Sign one time key. - QByteArray signOneTimeKey(const QString &key) const; + OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const; SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; - UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); + UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const; DeviceKeys deviceKeys() const; -- 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(-) 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 fe66480d4f6d96444ca090df2ae04dc7da033ea4 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 31 May 2022 19:43:01 +0200 Subject: Remove accounts from accountsLoading when they're no longer loading --- lib/accountregistry.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index b40d5ecf..289f20dd 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -99,20 +99,29 @@ void AccountRegistry::invokeLogin() AccountSettings account { accountId }; auto connection = new Connection(account.homeserver()); connect(connection, &Connection::connected, this, - [connection] { + [connection, this, accountId] { connection->loadState(); connection->setLazyLoading(true); connection->syncLoop(); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); }); connect(connection, &Connection::loginError, this, - [this, connection](const QString& error, + [this, connection, accountId](const QString& error, const QString& details) { emit loginError(connection, error, details); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); }); connect(connection, &Connection::resolveError, this, - [this, connection](const QString& error) { + [this, connection, accountId](const QString& error) { emit resolveError(connection, error); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); }); connection->assumeIdentity( account.userId(), accessTokenLoadingJob->binaryData(), -- cgit v1.2.3 From cd442611b19ec4a438d0847bf09b7bca99b494d3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 1 Jun 2022 08:05:21 +0200 Subject: Fix FTBFS after the merge --- autotests/testolmaccount.cpp | 2 +- lib/connection.cpp | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 60f4ab38..4b32393d 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -404,7 +404,7 @@ void TestOlmAccount::claimKeys() claimKeysJob->oneTimeKeys().value(userId).value(deviceId); for (auto it = oneTimeKeys.begin(); it != oneTimeKeys.end(); ++it) { if (it.key().startsWith(SignedCurve25519Key) - && it.value().isObject()) + && std::holds_alternative(it.value())) return; } QFAIL("The claimed one time key is not in /claim response"); diff --git a/lib/connection.cpp b/lib/connection.cpp index 102fb16d..7885718f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -376,7 +376,7 @@ public: const QByteArray& message) const; bool createOlmSession(const QString& targetUserId, const QString& targetDeviceId, - const QJsonObject& oneTimeKeyObject); + const OneTimeKeys &oneTimeKeyObject); QString curveKeyForUserDevice(const QString& userId, const QString& device) const; QString edKeyForUserDevice(const QString& userId, @@ -2306,7 +2306,7 @@ std::pair Connection::Private::olmEncryptMessage( bool Connection::Private::createOlmSession(const QString& targetUserId, const QString& targetDeviceId, - const QJsonObject& oneTimeKeyObject) + const OneTimeKeys& oneTimeKeyObject) { static QOlmUtility verifier; qDebug(E2EE) << "Creating a new session for" << targetUserId @@ -2316,17 +2316,23 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, << targetDeviceId; return false; } - auto signedOneTimeKey = oneTimeKeyObject.constBegin()->toObject(); + auto* signedOneTimeKey = + std::get_if(&*oneTimeKeyObject.begin()); + if (!signedOneTimeKey) { + qWarning(E2EE) << "No signed one time key for" << targetUserId + << targetDeviceId; + return false; + } // Verify contents of signedOneTimeKey - for that, drop `signatures` and // `unsigned` and then verify the object against the respective signature const auto signature = - signedOneTimeKey.take("signatures"_ls)[targetUserId]["ed25519:"_ls % targetDeviceId] - .toString() + signedOneTimeKey + ->signatures[targetUserId]["ed25519:"_ls % targetDeviceId] .toLatin1(); - signedOneTimeKey.remove("unsigned"_ls); if (!verifier.ed25519Verify( edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), - QJsonDocument(signedOneTimeKey).toJson(QJsonDocument::Compact), + QJsonDocument(toJson(SignedOneTimeKey { signedOneTimeKey->key, {} })) + .toJson(QJsonDocument::Compact), signature)) { qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId << targetDeviceId << ". Skipping this device."; @@ -2336,7 +2342,7 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, curveKeyForUserDevice(targetUserId, targetDeviceId); auto session = QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, - signedOneTimeKey["key"].toString()); + signedOneTimeKey->key); if (!session) { qCWarning(E2EE) << "Failed to create olm session for " << recipientCurveKey << session.error(); -- 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(-) 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/e2ee/qolmaccount.cpp | 4 ++-- lib/room.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 4cfc6151..241ae750 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -160,8 +160,8 @@ UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const { OneTimeKeys signedOneTimeKeys; - const auto& curveKeys = keys.curve25519(); - for (const auto& [keyId, key] : asKeyValueRange(curveKeys)) + for (const auto& curveKeys = keys.curve25519(); + const auto& [keyId, key] : asKeyValueRange(curveKeys)) signedOneTimeKeys["signed_curve25519:" % keyId] = signedOneTimeKey(key.toUtf8(), sign(QJsonObject{{"key", key}})); return signedOneTimeKeys; 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 de3a446b7b005ac57edb422eced7eec252ed3a92 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Jun 2022 13:38:32 +0200 Subject: GTAD: inline public_rooms_chunk.yaml Also: drop inlining PublicRoomResponse by the name because it's already inlined by $ref before that. This configuration needs the latest GTAD (revision 51c53ed3) to work correctly; earlier GTAD will produce FTBFS code. --- gtad/gtad.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index b1c143b6..2d004cbc 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -97,10 +97,10 @@ analyzer: title: MsisdnValidationData - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<> - /public_rooms_response.yaml$/: { _inline: true } + - /public_rooms_chunk.yaml$/: { _inline: true } - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types - schema: - getTurnServer<: *QJsonObject # It's used as an opaque JSON object - - PublicRoomResponse: { _inline: true } # - defineFilter>: &Filter # Force folding into a structure # type: Filter # imports: '"csapi/definitions/sync_filter.h"' -- cgit v1.2.3 From c9a2c38b5645de22315aac35c78a40127b5d2fe9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Jun 2022 14:02:17 +0200 Subject: Regenerate API files This only updates 3 files affected by the change in the previous commit. --- lib/csapi/definitions/public_rooms_response.h | 69 ++++++++++++++++++++++++-- lib/csapi/list_public_rooms.h | 10 ++-- lib/csapi/space_hierarchy.h | 71 +++++++++++++++++++++++++-- 3 files changed, 139 insertions(+), 11 deletions(-) diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index ca512280..da917a98 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -6,13 +6,76 @@ #include "converters.h" -#include "csapi/definitions/public_rooms_chunk.h" - namespace Quotient { + +struct PublicRoomsChunk { + /// The canonical alias of the room, if any. + QString canonicalAlias; + + /// The name of the room, if any. + QString name; + + /// The number of members joined to the room. + int numJoinedMembers; + + /// The ID of the room. + QString roomId; + + /// The topic of the room, if any. + QString topic; + + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; + + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; + + /// The URL for the room's avatar, if one is set. + QUrl avatarUrl; + + /// The room's join rule. When not present, the room is assumed to + /// be `public`. Note that rooms with `invite` join rules are not + /// expected here, but rooms with `knock` rules are given their + /// near-public nature. + QString joinRule; +}; + +template <> +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) + { + addParam(jo, QStringLiteral("canonical_alias"), + pod.canonicalAlias); + addParam(jo, QStringLiteral("name"), pod.name); + addParam<>(jo, QStringLiteral("num_joined_members"), + pod.numJoinedMembers); + addParam<>(jo, QStringLiteral("room_id"), pod.roomId); + addParam(jo, QStringLiteral("topic"), pod.topic); + addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); + addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); + addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); + addParam(jo, QStringLiteral("join_rule"), pod.joinRule); + } + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) + { + fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); + fromJson(jo.value("name"_ls), pod.name); + fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); + fromJson(jo.value("room_id"_ls), pod.roomId); + fromJson(jo.value("topic"_ls), pod.topic); + fromJson(jo.value("world_readable"_ls), pod.worldReadable); + fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); + fromJson(jo.value("join_rule"_ls), pod.joinRule); + } +}; + /// A list of the rooms on the server. struct PublicRoomsResponse { /// A paginated chunk of public rooms. - QVector chunk; + QVector chunk; /// A pagination token for the response. The absence of this token /// means there are no more results to fetch and the client should diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 701a74f6..e1f03db7 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -4,7 +4,7 @@ #pragma once -#include "csapi/definitions/public_rooms_chunk.h" +#include "csapi/definitions/public_rooms_response.h" #include "jobs/basejob.h" @@ -103,9 +103,9 @@ public: // Result properties /// A paginated chunk of public rooms. - QVector chunk() const + QVector chunk() const { - return loadFromJson>("chunk"_ls); + return loadFromJson>("chunk"_ls); } /// A pagination token for the response. The absence of this token @@ -182,9 +182,9 @@ public: // Result properties /// A paginated chunk of public rooms. - QVector chunk() const + QVector chunk() const { - return loadFromJson>("chunk"_ls); + return loadFromJson>("chunk"_ls); } /// A pagination token for the response. The absence of this token diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h index adffe2f8..5b3c1f80 100644 --- a/lib/csapi/space_hierarchy.h +++ b/lib/csapi/space_hierarchy.h @@ -5,7 +5,6 @@ #pragma once #include "csapi/../../event-schemas/schema/core-event-schema/stripped_state.h" -#include "csapi/definitions/public_rooms_chunk.h" #include "jobs/basejob.h" @@ -25,6 +24,53 @@ namespace Quotient { */ class QUOTIENT_API GetSpaceHierarchyJob : public BaseJob { public: + // Inner data structures + + /// Paginates over the space tree in a depth-first manner to locate child + /// rooms of a given space. + /// + /// Where a child room is unknown to the local server, federation is used to + /// fill in the details. The servers listed in the `via` array should be + /// contacted to attempt to fill in missing rooms. + /// + /// Only [`m.space.child`](#mspacechild) state events of the room are + /// considered. Invalid child rooms and parent events are not covered by + /// this endpoint. + struct ChildRoomsChunk { + /// The canonical alias of the room, if any. + QString canonicalAlias; + /// The name of the room, if any. + QString name; + /// The number of members joined to the room. + int numJoinedMembers; + /// The ID of the room. + QString roomId; + /// The topic of the room, if any. + QString topic; + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; + /// The URL for the room's avatar, if one is set. + QUrl avatarUrl; + /// The room's join rule. When not present, the room is assumed to + /// be `public`. + QString joinRule; + /// The `type` of room (from + /// [`m.room.create`](/client-server-api/#mroomcreate)), if any. + QString roomType; + /// The [`m.space.child`](#mspacechild) events of the space-room, + /// represented as [Stripped State Events](#stripped-state) with an + /// added `origin_server_ts` key. + /// + /// If the room is not a space-room, this should be empty. + QVector childrenState; + }; + + // Construction/destruction + /*! \brief Retrieve a portion of a space tree. * * \param roomId @@ -75,9 +121,9 @@ public: // Result properties /// The rooms for the current page, with the current filters. - QVector rooms() const + QVector rooms() const { - return loadFromJson>("rooms"_ls); + return loadFromJson>("rooms"_ls); } /// A token to supply to `from` to keep paginating the responses. Not @@ -85,4 +131,23 @@ public: QString nextBatch() const { return loadFromJson("next_batch"_ls); } }; +template <> +struct JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, + GetSpaceHierarchyJob::ChildRoomsChunk& result) + { + fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); + fromJson(jo.value("name"_ls), result.name); + fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("topic"_ls), result.topic); + fromJson(jo.value("world_readable"_ls), result.worldReadable); + fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + fromJson(jo.value("join_rule"_ls), result.joinRule); + fromJson(jo.value("room_type"_ls), result.roomType); + fromJson(jo.value("children_state"_ls), result.childrenState); + } +}; + } // namespace Quotient -- cgit v1.2.3 From f10259aa3b5051e4b36b4e0fd2f2d0db06fb7c20 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Jun 2022 13:55:15 +0200 Subject: Add GTAD as a submodule Code generation in libQuotient is pretty sensitive to GTAD version (or even a particular commit at times); so it makes sense to have GTAD as a submodule in order to control the revision CI uses. (amended with the GTAD commit that uses the right yaml-cpp commit) --- .gitmodules | 3 +++ gtad/gtad | 1 + 2 files changed, 4 insertions(+) create mode 160000 gtad/gtad diff --git a/.gitmodules b/.gitmodules index e69de29b..f3aef316 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gtad/gtad"] + path = gtad/gtad + url = https://github.com/quotient-im/gtad.git diff --git a/gtad/gtad b/gtad/gtad new file mode 160000 index 00000000..fcc8e0f2 --- /dev/null +++ b/gtad/gtad @@ -0,0 +1 @@ +Subproject commit fcc8e0f28367f37890db9cfa5e96d08d599b36fc -- cgit v1.2.3 From a2ba9119c6b20d227f75e9a3427df06b4923ee89 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Jun 2022 14:01:39 +0200 Subject: CI: use GTAD submodule Also: make all cloning for update-api shallow, for optimisation. --- .github/workflows/ci.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac67b3b..9c777cbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,14 +175,13 @@ jobs: cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS cmake --build qtkeychain/build --target install - - name: Pull CS API and build GTAD + - name: get CS API definitions; clone and build GTAD if: matrix.update-api - working-directory: ${{ runner.workspace }} run: | - git clone https://github.com/quotient-im/matrix-spec.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - cmake -S gtad -B build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF - cmake --build build/gtad + git clone --depth=1 https://github.com/quotient-im/matrix-spec.git ../matrix-spec + git submodule update --init --recursive --depth=1 + cmake -S gtad/gtad -B ../build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF + cmake --build ../build/gtad echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_SPEC_PATH=${{ runner.workspace }}/matrix-spec \ -DGTAD_PATH=${{ runner.workspace }}/build/gtad/gtad" \ >>$GITHUB_ENV -- cgit v1.2.3 From d547e84c9335d9524ae7530be622d5ed2f0b1fb8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 8 Jun 2022 13:58:33 +0200 Subject: Save connection state while QCoreApplication is still there This reimplements #558 in a more reliable way. Deconstruction of AccountRegistry may (or may not, yay for static initialisation) occur after deconstruction of QCoreApplication, in which case an attempt to determine the directory for the state fails because it depends on the application object existence. --- lib/accountregistry.cpp | 7 ------- lib/accountregistry.h | 2 -- lib/connection.cpp | 1 + 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 2cab54a3..ad7c5f99 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -135,10 +135,3 @@ QStringList AccountRegistry::accountsLoading() const { return m_accountsLoading; } - -AccountRegistry::~AccountRegistry() -{ - for (const auto& connection : *this) { - connection->saveState(); - } -} diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 959c7d42..38cfe6c6 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -42,8 +42,6 @@ public: [[deprecated("Use Accounts variable instead")]] // static AccountRegistry& instance(); - ~AccountRegistry(); - // Expose most of QVector's const-API but only provide add() and drop() // for changing it. In theory other changing operations could be supported // too; but then boilerplate begin/end*() calls has to be tucked into each diff --git a/lib/connection.cpp b/lib/connection.cpp index 7885718f..101bef89 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -611,6 +611,7 @@ void Connection::Private::completeSetup(const QString& mxId) << "by user" << data->userId() << "from device" << data->deviceId(); Accounts.add(q); + connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED -- cgit v1.2.3 From 40311ec08a88de821884197bc60f1b48748cbbb3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Jun 2022 16:51:28 +0200 Subject: gtad.yaml: more clarifying comments --- gtad/gtad.yaml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 2d004cbc..12a27c06 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -88,7 +88,8 @@ analyzer: - /(room|client)_event.yaml$/: RoomEventPtr - /event(_without_room_id)?.yaml$/: EventPtr - +set: - # This renderer actually applies to all $ref things + # This renderer applies to everything actually $ref'ed + # (not substituted) _importRenderer: '"{{#segments}}{{_}}{{#_join}}/{{/_join}}{{/segments}}.h"' +on: - '/^(\./)?definitions/request_email_validation.yaml$/': @@ -96,7 +97,20 @@ analyzer: - '/^(\./)?definitions/request_msisdn_validation.yaml$/': title: MsisdnValidationData - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<> + + # Despite being used in two calls, it's more practical to have those + # fields available as getters right from the respective job classes - /public_rooms_response.yaml$/: { _inline: true } + + # list_public_rooms.yaml (via public_rooms_response.yaml) and + # space_hierarchy.yaml use public_rooms_chunk.yaml as a common base + # structure, adding (space_hiearchy) or overriding + # (public_rooms_response) fields for their purposes. The spec text + # confusingly ends up with having two different structures named + # "PublicRoomsChunk". To make sure the types are distinct in + # libQuotient, this common base is inlined into the actually used + # data structures (that have distinct names) defined + # in space_hierarchy.h and public_rooms_response.h, respectively - /public_rooms_chunk.yaml$/: { _inline: true } - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types - schema: @@ -117,17 +131,17 @@ analyzer: - /^Notification|Result$/: type: "std::vector<{{1}}>" imports: '"events/eventloader.h"' - - /state_event.yaml$/: StateEvents - - /(room|client)_event.yaml$/: RoomEvents - - /event(_without_room_id)?.yaml$/: Events + - /state_event.yaml$/: StateEvents # 'imports' already set under $ref + - /(room|client)_event.yaml$/: RoomEvents # ditto + - /event(_without_room_id)?.yaml$/: Events # ditto - //: "QVector<{{1}}>" - map: # `additionalProperties` in OpenAPI - RoomState: type: "UnorderedMap" moveOnly: - /.+/: "QHash" - - //: QVariantHash - - variant: # A sequence `type` (multitype) in OpenAPI + - //: QVariantHash # QJsonObject?.. + - variant: # A sequence `type` or a 'oneOf' group in OpenAPI - /^string,null|null,string$/: *QString - //: QVariant -- cgit v1.2.3 From c11dc723e8b5170e6fd3750cffe5990f13772cdd Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Jun 2022 10:22:50 +0200 Subject: gtad: update submodule --- gtad/gtad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/gtad b/gtad/gtad index fcc8e0f2..9e89412e 160000 --- a/gtad/gtad +++ b/gtad/gtad @@ -1 +1 @@ -Subproject commit fcc8e0f28367f37890db9cfa5e96d08d599b36fc +Subproject commit 9e89412ec0c8d792e525a660940ab12d3fa5cf9c -- cgit v1.2.3 From f16e4d4e3e293a646569cb765a22607e4fd69756 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Jun 2022 15:12:52 +0200 Subject: Regenerate API files The latest GTAD no more emits public_rooms_chunk.h (public_rooms_response.h already has the same definition), and skips on PublicRoomsResponse structure that is never used. --- lib/csapi/definitions/public_rooms_chunk.h | 73 --------------------------- lib/csapi/definitions/public_rooms_response.h | 40 --------------- 2 files changed, 113 deletions(-) delete mode 100644 lib/csapi/definitions/public_rooms_chunk.h diff --git a/lib/csapi/definitions/public_rooms_chunk.h b/lib/csapi/definitions/public_rooms_chunk.h deleted file mode 100644 index eac68213..00000000 --- a/lib/csapi/definitions/public_rooms_chunk.h +++ /dev/null @@ -1,73 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "converters.h" - -namespace Quotient { - -struct PublicRoomsChunk { - /// The canonical alias of the room, if any. - QString canonicalAlias; - - /// The name of the room, if any. - QString name; - - /// The number of members joined to the room. - int numJoinedMembers; - - /// The ID of the room. - QString roomId; - - /// The topic of the room, if any. - QString topic; - - /// Whether the room may be viewed by guest users without joining. - bool worldReadable; - - /// Whether guest users may join the room and participate in it. - /// If they can, they will be subject to ordinary power level - /// rules like any other user. - bool guestCanJoin; - - /// The URL for the room's avatar, if one is set. - QUrl avatarUrl; - - /// The room's join rule. When not present, the room is assumed to - /// be `public`. - QString joinRule; -}; - -template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) - { - addParam(jo, QStringLiteral("canonical_alias"), - pod.canonicalAlias); - addParam(jo, QStringLiteral("name"), pod.name); - addParam<>(jo, QStringLiteral("num_joined_members"), - pod.numJoinedMembers); - addParam<>(jo, QStringLiteral("room_id"), pod.roomId); - addParam(jo, QStringLiteral("topic"), pod.topic); - addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); - addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); - addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); - addParam(jo, QStringLiteral("join_rule"), pod.joinRule); - } - static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) - { - fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); - fromJson(jo.value("name"_ls), pod.name); - fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); - fromJson(jo.value("room_id"_ls), pod.roomId); - fromJson(jo.value("topic"_ls), pod.topic); - fromJson(jo.value("world_readable"_ls), pod.worldReadable); - fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); - fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); - fromJson(jo.value("join_rule"_ls), pod.joinRule); - } -}; - -} // namespace Quotient diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index da917a98..d0a2595c 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -72,44 +72,4 @@ struct JsonObjectConverter { } }; -/// A list of the rooms on the server. -struct PublicRoomsResponse { - /// A paginated chunk of public rooms. - QVector chunk; - - /// A pagination token for the response. The absence of this token - /// means there are no more results to fetch and the client should - /// stop paginating. - QString nextBatch; - - /// A pagination token that allows fetching previous results. The - /// absence of this token means there are no results before this - /// batch, i.e. this is the first batch. - QString prevBatch; - - /// An estimate on the total number of public rooms, if the - /// server has an estimate. - Omittable totalRoomCountEstimate; -}; - -template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod) - { - addParam<>(jo, QStringLiteral("chunk"), pod.chunk); - addParam(jo, QStringLiteral("next_batch"), pod.nextBatch); - addParam(jo, QStringLiteral("prev_batch"), pod.prevBatch); - addParam(jo, QStringLiteral("total_room_count_estimate"), - pod.totalRoomCountEstimate); - } - static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod) - { - fromJson(jo.value("chunk"_ls), pod.chunk); - fromJson(jo.value("next_batch"_ls), pod.nextBatch); - fromJson(jo.value("prev_batch"_ls), pod.prevBatch); - fromJson(jo.value("total_room_count_estimate"_ls), - pod.totalRoomCountEstimate); - } -}; - } // namespace Quotient -- cgit v1.2.3 From 743909a728e22f52f7531b0f98395f0361cd0fd3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Jun 2022 19:41:59 +0200 Subject: gtad.yaml: Treat child rooms state as events --- gtad/gtad.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 12a27c06..f4ad122e 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -119,6 +119,7 @@ analyzer: # type: Filter # imports: '"csapi/definitions/sync_filter.h"' # - getFilter<: *Filter + - StrippedChildStateEvent: void # only used in an array, see below - RoomFilter: # A structure inside Filter, same story as with *_filter.yaml - OneTimeKeys: type: OneTimeKeys @@ -128,8 +129,9 @@ analyzer: - string: QStringList - +set: { moveOnly: } +on: - - /^Notification|Result$/: - type: "std::vector<{{1}}>" + - /^Notification|Result|ChildRoomsChunk$/: "std::vector<{{1}}>" + - StrippedChildStateEvent: + type: StateEvents imports: '"events/eventloader.h"' - /state_event.yaml$/: StateEvents # 'imports' already set under $ref - /(room|client)_event.yaml$/: RoomEvents # ditto -- cgit v1.2.3 From 130c3ee975fe5312e97080cd978c0807b2611a68 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Jun 2022 21:33:21 +0200 Subject: gtad.yaml: Drop deprecated home_server field from login/register --- gtad/gtad.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index f4ad122e..0bec3b7a 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -29,6 +29,8 @@ analyzer: login>/user: "" login>/medium: "" login>/address: "" + login -- cgit v1.2.3 From 3a2e7f19c97289fb962b1c0ba4439870bbd0f31d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Jun 2022 17:03:06 +0200 Subject: gtad: update submodule (again) --- gtad/gtad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtad/gtad b/gtad/gtad index 9e89412e..9ea32fb7 160000 --- a/gtad/gtad +++ b/gtad/gtad @@ -1 +1 @@ -Subproject commit 9e89412ec0c8d792e525a660940ab12d3fa5cf9c +Subproject commit 9ea32fb74767a62a3a0d27b3b181e8c18fb0c691 -- cgit v1.2.3 From ed05d9d589fe3d36bd3714e648016352179afbef Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 11 Jun 2022 19:42:08 +0200 Subject: Regenerate API files using latest matrix-spec New: - refresh tokens support (changes in login.* and registration.*; RefreshJob); - GetRelatingEvents[WithRelType[AndEventType]]Job Changed space_hierarchy.*: - childrenState is of type StateEvents now; limit and maxDepth are (omittable) integers, not doubles. - no more unused `stripped_state.h` file inclusion. --- .../schema/core-event-schema/stripped_state.h | 44 ---- lib/csapi/login.cpp | 4 +- lib/csapi/login.h | 30 ++- lib/csapi/refresh.cpp | 17 ++ lib/csapi/refresh.h | 65 +++++ lib/csapi/registration.cpp | 4 +- lib/csapi/registration.h | 32 ++- lib/csapi/relations.cpp | 111 +++++++++ lib/csapi/relations.h | 277 +++++++++++++++++++++ lib/csapi/space_hierarchy.cpp | 12 +- lib/csapi/space_hierarchy.h | 17 +- 11 files changed, 535 insertions(+), 78 deletions(-) delete mode 100644 event-schemas/schema/core-event-schema/stripped_state.h create mode 100644 lib/csapi/refresh.cpp create mode 100644 lib/csapi/refresh.h create mode 100644 lib/csapi/relations.cpp create mode 100644 lib/csapi/relations.h diff --git a/event-schemas/schema/core-event-schema/stripped_state.h b/event-schemas/schema/core-event-schema/stripped_state.h deleted file mode 100644 index 742b0a56..00000000 --- a/event-schemas/schema/core-event-schema/stripped_state.h +++ /dev/null @@ -1,44 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "converters.h" - -namespace Quotient { -/// A stripped down state event, with only the `type`, `state_key`, -/// `sender`, and `content` keys. -struct StrippedStateEvent { - /// The `content` for the event. - QJsonObject content; - - /// The `state_key` for the event. - QString stateKey; - - /// The `type` for the event. - QString type; - - /// The `sender` for the event. - QString sender; -}; - -template <> -struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const StrippedStateEvent& pod) - { - addParam<>(jo, QStringLiteral("content"), pod.content); - addParam<>(jo, QStringLiteral("state_key"), pod.stateKey); - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<>(jo, QStringLiteral("sender"), pod.sender); - } - static void fillFrom(const QJsonObject& jo, StrippedStateEvent& pod) - { - fromJson(jo.value("content"_ls), pod.content); - fromJson(jo.value("state_key"_ls), pod.stateKey); - fromJson(jo.value("type"_ls), pod.type); - fromJson(jo.value("sender"_ls), pod.sender); - } -}; - -} // namespace Quotient diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index b2175a05..5e007d8f 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -21,7 +21,8 @@ LoginJob::LoginJob(const QString& type, const Omittable& identifier, const QString& password, const QString& token, const QString& deviceId, - const QString& initialDeviceDisplayName) + const QString& initialDeviceDisplayName, + Omittable refreshToken) : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), makePath("/_matrix/client/v3", "/login"), false) { @@ -33,5 +34,6 @@ LoginJob::LoginJob(const QString& type, addParam(_data, QStringLiteral("device_id"), deviceId); addParam(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); + addParam(_data, QStringLiteral("refresh_token"), refreshToken); setRequestData(std::move(_data)); } diff --git a/lib/csapi/login.h b/lib/csapi/login.h index ce6951eb..b9f14266 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -111,12 +111,16 @@ public: * \param initialDeviceDisplayName * A display name to assign to the newly-created device. Ignored * if `device_id` corresponds to a known device. + * + * \param refreshToken + * If true, the client supports refresh tokens. */ explicit LoginJob(const QString& type, const Omittable& identifier = none, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, - const QString& initialDeviceDisplayName = {}); + const QString& initialDeviceDisplayName = {}, + Omittable refreshToken = none); // Result properties @@ -130,15 +134,23 @@ public: return loadFromJson("access_token"_ls); } - /// The server_name of the homeserver on which the account has - /// been registered. - /// - /// **Deprecated**. Clients should extract the server_name from - /// `user_id` (by splitting at the first colon) if they require - /// it. Note also that `homeserver` is not spelt this way. - QString homeServer() const + /// A refresh token for the account. This token can be used to + /// obtain a new access token when it expires by calling the + /// `/refresh` endpoint. + QString refreshToken() const + { + return loadFromJson("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. Once + /// the access token has expired a new access token can be + /// obtained by using the provided refresh token. If no + /// refresh token is provided, the client will need to re-log in + /// to obtain a new access token. If not given, the client can + /// assume that the access token will not expire. + Omittable expiresInMs() const { - return loadFromJson("home_server"_ls); + return loadFromJson>("expires_in_ms"_ls); } /// ID of the logged-in device. Will be the same as the diff --git a/lib/csapi/refresh.cpp b/lib/csapi/refresh.cpp new file mode 100644 index 00000000..8d4a34ae --- /dev/null +++ b/lib/csapi/refresh.cpp @@ -0,0 +1,17 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "refresh.h" + +using namespace Quotient; + +RefreshJob::RefreshJob(const QString& refreshToken) + : BaseJob(HttpVerb::Post, QStringLiteral("RefreshJob"), + makePath("/_matrix/client/v3", "/refresh"), false) +{ + QJsonObject _data; + addParam(_data, QStringLiteral("refresh_token"), refreshToken); + setRequestData(std::move(_data)); + addExpectedKey("access_token"); +} diff --git a/lib/csapi/refresh.h b/lib/csapi/refresh.h new file mode 100644 index 00000000..d432802c --- /dev/null +++ b/lib/csapi/refresh.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Refresh an access token + * + * Refresh an access token. Clients should use the returned access token + * when making subsequent API calls, and store the returned refresh token + * (if given) in order to refresh the new access token when necessary. + * + * After an access token has been refreshed, a server can choose to + * invalidate the old access token immediately, or can choose not to, for + * example if the access token would expire soon anyways. Clients should + * not make any assumptions about the old access token still being valid, + * and should use the newly provided access token instead. + * + * The old refresh token remains valid until the new access token or refresh + * token is used, at which point the old refresh token is revoked. + * + * Note that this endpoint does not require authentication via an + * access token. Authentication is provided via the refresh token. + * + * Application Service identity assertion is disabled for this endpoint. + */ +class QUOTIENT_API RefreshJob : public BaseJob { +public: + /*! \brief Refresh an access token + * + * \param refreshToken + * The refresh token + */ + explicit RefreshJob(const QString& refreshToken = {}); + + // Result properties + + /// The new access token to use. + QString accessToken() const + { + return loadFromJson("access_token"_ls); + } + + /// The new refresh token to use when the access token needs to + /// be refreshed again. If not given, the old refresh token can + /// be re-used. + QString refreshToken() const + { + return loadFromJson("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. If not + /// given, the client can assume that the access token will not + /// expire. + Omittable expiresInMs() const + { + return loadFromJson>("expires_in_ms"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index aa4b4fad..3541724b 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -18,7 +18,8 @@ RegisterJob::RegisterJob(const QString& kind, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, - Omittable inhibitLogin) + Omittable inhibitLogin, + Omittable refreshToken) : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), makePath("/_matrix/client/v3", "/register"), queryToRegister(kind), {}, false) @@ -31,6 +32,7 @@ RegisterJob::RegisterJob(const QString& kind, addParam(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); addParam(_data, QStringLiteral("inhibit_login"), inhibitLogin); + addParam(_data, QStringLiteral("refresh_token"), refreshToken); setRequestData(std::move(_data)); addExpectedKey("user_id"); } diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 39840008..7a20cab8 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -93,6 +93,9 @@ public: * If true, an `access_token` and `device_id` should not be * returned from this call, therefore preventing an automatic * login. Defaults to false. + * + * \param refreshToken + * If true, the client supports refresh tokens. */ explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable& auth = none, @@ -100,7 +103,8 @@ public: const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, - Omittable inhibitLogin = none); + Omittable inhibitLogin = none, + Omittable refreshToken = none); // Result properties @@ -118,15 +122,27 @@ public: return loadFromJson("access_token"_ls); } - /// The server_name of the homeserver on which the account has - /// been registered. + /// A refresh token for the account. This token can be used to + /// obtain a new access token when it expires by calling the + /// `/refresh` endpoint. + /// + /// Omitted if the `inhibit_login` option is false. + QString refreshToken() const + { + return loadFromJson("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. Once + /// the access token has expired a new access token can be + /// obtained by using the provided refresh token. If no + /// refresh token is provided, the client will need to re-log in + /// to obtain a new access token. If not given, the client can + /// assume that the access token will not expire. /// - /// **Deprecated**. Clients should extract the server_name from - /// `user_id` (by splitting at the first colon) if they require - /// it. Note also that `homeserver` is not spelt this way. - QString homeServer() const + /// Omitted if the `inhibit_login` option is false. + Omittable expiresInMs() const { - return loadFromJson("home_server"_ls); + return loadFromJson>("expires_in_ms"_ls); } /// ID of the registered device. Will be the same as the diff --git a/lib/csapi/relations.cpp b/lib/csapi/relations.cpp new file mode 100644 index 00000000..8bcecee4 --- /dev/null +++ b/lib/csapi/relations.cpp @@ -0,0 +1,111 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "relations.h" + +using namespace Quotient; + +auto queryToGetRelatingEvents(const QString& from, const QString& to, + Omittable limit) +{ + QUrlQuery _q; + addParam(_q, QStringLiteral("from"), from); + addParam(_q, QStringLiteral("to"), to); + addParam(_q, QStringLiteral("limit"), limit); + return _q; +} + +QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + const QString& from, const QString& to, + Omittable limit) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/relations/", eventId), + queryToGetRelatingEvents(from, to, limit)); +} + +GetRelatingEventsJob::GetRelatingEventsJob(const QString& roomId, + const QString& eventId, + const QString& from, + const QString& to, + Omittable limit) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId), + queryToGetRelatingEvents(from, to, limit)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to, + Omittable limit) +{ + QUrlQuery _q; + addParam(_q, QStringLiteral("from"), from); + addParam(_q, QStringLiteral("to"), to); + addParam(_q, QStringLiteral("limit"), limit); + return _q; +} + +QUrl GetRelatingEventsWithRelTypeJob::makeRequestUrl( + QUrl baseUrl, const QString& roomId, const QString& eventId, + const QString& relType, const QString& from, const QString& to, + Omittable limit) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit)); +} + +GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from, const QString& to, Omittable limit) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from, + const QString& to, + Omittable limit) +{ + QUrlQuery _q; + addParam(_q, QStringLiteral("from"), from); + addParam(_q, QStringLiteral("to"), to); + addParam(_q, QStringLiteral("limit"), limit); + return _q; +} + +QUrl GetRelatingEventsWithRelTypeAndEventTypeJob::makeRequestUrl( + QUrl baseUrl, const QString& roomId, const QString& eventId, + const QString& relType, const QString& eventType, const QString& from, + const QString& to, Omittable limit) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit)); +} + +GetRelatingEventsWithRelTypeAndEventTypeJob:: + GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from, const QString& to, + Omittable limit) + : BaseJob(HttpVerb::Get, + QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h new file mode 100644 index 00000000..985a43b5 --- /dev/null +++ b/lib/csapi/relations.h @@ -0,0 +1,277 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/eventloader.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Get the child events for a given parent event. + * + * Retrieve all of the child events for a given parent event. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsJob : public BaseJob { +public: + /*! \brief Get the child events for a given parent event. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` token from a previous call, or a returned + * `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + */ + explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId, + const QString& from = {}, + const QString& to = {}, + Omittable limit = none); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRelatingEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& from = {}, + const QString& to = {}, + Omittable limit = none); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. + RoomEvents chunk() { return takeFromJson("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson("prev_batch"_ls); } +}; + +/*! \brief Get the child events for a given parent event, with a given + * `relType`. + * + * Retrieve all of the child events for a given parent event which relate to the + * parent using the given `relType`. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsWithRelTypeJob : public BaseJob { +public: + /*! \brief Get the child events for a given parent event, with a given + * `relType`. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param relType + * The [relationship type](/client-server-api/#relationship-types) to + * search for. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` token from a previous call, or a returned + * `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + */ + explicit GetRelatingEventsWithRelTypeJob(const QString& roomId, + const QString& eventId, + const QString& relType, + const QString& from = {}, + const QString& to = {}, + Omittable limit = none); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRelatingEventsWithRelTypeJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& relType, + const QString& from = {}, const QString& to = {}, + Omittable limit = none); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. The events returned will match the `relType` + /// supplied in the URL. + RoomEvents chunk() { return takeFromJson("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson("prev_batch"_ls); } +}; + +/*! \brief Get the child events for a given parent event, with a given `relType` + * and `eventType`. + * + * Retrieve all of the child events for a given parent event which relate to the + * parent using the given `relType` and have the given `eventType`. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsWithRelTypeAndEventTypeJob + : public BaseJob { +public: + /*! \brief Get the child events for a given parent event, with a given + * `relType` and `eventType`. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param relType + * The [relationship type](/client-server-api/#relationship-types) to + * search for. + * + * \param eventType + * The event type of child events to search for. + * + * Note that in encrypted rooms this will typically always be + * `m.room.encrypted` regardless of the event type contained within the + * encrypted payload. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` token from a previous call, or a returned + * `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + */ + explicit GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from = {}, + const QString& to = {}, Omittable limit = none); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetRelatingEventsWithRelTypeAndEventTypeJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& relType, + const QString& eventType, + const QString& from = {}, const QString& to = {}, + Omittable limit = none); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. The events returned will match the `relType` and + /// `eventType` supplied in the URL. + RoomEvents chunk() { return takeFromJson("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson("prev_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/space_hierarchy.cpp b/lib/csapi/space_hierarchy.cpp index 7c17ce8a..7b5c7eac 100644 --- a/lib/csapi/space_hierarchy.cpp +++ b/lib/csapi/space_hierarchy.cpp @@ -7,8 +7,8 @@ using namespace Quotient; auto queryToGetSpaceHierarchy(Omittable suggestedOnly, - Omittable limit, - Omittable maxDepth, const QString& from) + Omittable limit, Omittable maxDepth, + const QString& from) { QUrlQuery _q; addParam(_q, QStringLiteral("suggested_only"), suggestedOnly); @@ -20,8 +20,8 @@ auto queryToGetSpaceHierarchy(Omittable suggestedOnly, QUrl GetSpaceHierarchyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, Omittable suggestedOnly, - Omittable limit, - Omittable maxDepth, + Omittable limit, + Omittable maxDepth, const QString& from) { return BaseJob::makeRequestUrl( @@ -32,8 +32,8 @@ QUrl GetSpaceHierarchyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, GetSpaceHierarchyJob::GetSpaceHierarchyJob(const QString& roomId, Omittable suggestedOnly, - Omittable limit, - Omittable maxDepth, + Omittable limit, + Omittable maxDepth, const QString& from) : BaseJob(HttpVerb::Get, QStringLiteral("GetSpaceHierarchyJob"), makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h index 5b3c1f80..7a421be8 100644 --- a/lib/csapi/space_hierarchy.h +++ b/lib/csapi/space_hierarchy.h @@ -4,8 +4,7 @@ #pragma once -#include "csapi/../../event-schemas/schema/core-event-schema/stripped_state.h" - +#include "events/eventloader.h" #include "jobs/basejob.h" namespace Quotient { @@ -66,7 +65,7 @@ public: /// added `origin_server_ts` key. /// /// If the room is not a space-room, this should be empty. - QVector childrenState; + StateEvents childrenState; }; // Construction/destruction @@ -103,8 +102,8 @@ public: */ explicit GetSpaceHierarchyJob(const QString& roomId, Omittable suggestedOnly = none, - Omittable limit = none, - Omittable maxDepth = none, + Omittable limit = none, + Omittable maxDepth = none, const QString& from = {}); /*! \brief Construct a URL without creating a full-fledged job object @@ -114,16 +113,16 @@ public: */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, Omittable suggestedOnly = none, - Omittable limit = none, - Omittable maxDepth = none, + Omittable limit = none, + Omittable maxDepth = none, const QString& from = {}); // Result properties /// The rooms for the current page, with the current filters. - QVector rooms() const + std::vector rooms() { - return loadFromJson>("rooms"_ls); + return takeFromJson>("rooms"_ls); } /// A token to supply to `from` to keep paginating the responses. Not -- cgit v1.2.3 From 9e594bd1d49dc0e1fdb8b74cef11fe3bfa3fdc1e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Jun 2022 10:32:40 +0200 Subject: CI: No more allow failure of update-api jobs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c777cbc..3422b5b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: ci-${{ github.ref }} jobs: CI: runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.update-api != '' }} # the current upstream API definitions are expected to fail the test + continue-on-error: false strategy: fail-fast: false max-parallel: 1 -- cgit v1.2.3 From 153981c0c3b8bfe82b4c5b773ae6f361afce14ed Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Jun 2022 12:51:36 +0200 Subject: Require CMake 3.16; extend C++20 to headers ...meaning, clients have to compile in C++20 mode too from now. --- CMakeLists.txt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f9ca9d2..efdd5bb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,4 @@ -# Officially CMake 3.16+ is needed but LGTM.com still sits on eoan that only -# has CMake 3.13 -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.16) if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() @@ -295,13 +293,10 @@ set_target_properties(${PROJECT_NAME} PROPERTIES set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY COMPATIBLE_INTERFACE_STRING ${PROJECT_NAME}_MAJOR_VERSION) -# C++17 required, C++20 desired (see above) -target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) -# TODO: Bump the CMake requirement and drop the version check here once -# LGTM upgrades -if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" - AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 +# Don't use PCH w/GCC (https://bugzilla.redhat.com/show_bug.cgi?id=1721553#c34) +if (NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) endif () -- cgit v1.2.3 From 7d4b46e6daf656a1e97426cb1f2f8c99c68c4dda Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 12 Jun 2022 14:16:12 +0200 Subject: Reduce the number of CI jobs It takes well over an hour to build the whole lineup for now; while the single right fix for that is making quotest capable of running in parallel, a few GCC jobs can be safely dropped for now (and we'll see if they should be brought back when parallel quotest unleashes the CI). --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c777cbc..99fe7b7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: max-parallel: 1 matrix: os: [ ubuntu-20.04, macos-10.15 ] - compiler: [ GCC, Clang ] + compiler: [ Clang ] # GCC builds are added individually below qt-version: [ '5.12.12' ] # Not using binary values here, to make the job captions more readable e2ee: [ '', e2ee ] @@ -29,8 +29,6 @@ jobs: platform: [ '' ] qt-arch: [ '' ] exclude: - - os: macos-10.15 - compiler: GCC - os: windows-2019 e2ee: e2ee # Not supported by the current CI script - os: macos-10.15 @@ -41,6 +39,11 @@ jobs: qt-version: '5.12.12' e2ee: e2ee sonar: sonar + - os: ubuntu-20.04 + compiler: GCC + qt-version: '5.12.12' + e2ee: e2ee + update-api: update-api - os: windows-2019 compiler: MSVC platform: x64 -- cgit v1.2.3 From 9ba97217e2ee6378cc7cf338828d093b81e6b8f4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 13 Jun 2022 20:51:06 +0200 Subject: Refresh documentation CONTRIBUTING.md got bitrotten in quite a few places. [skip ci] --- CONTRIBUTING.md | 286 +++++++++++++++++++++++++++++--------------------------- README.md | 2 +- 2 files changed, 150 insertions(+), 138 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3eeac68c..bc65abf3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,8 +35,8 @@ See the GitHub Help [articles about pull requests](https://help.github.com/artic to learn how to deal with them. We recommend creating different branches for different (logical) -changes, and creating a pull request when you're done into the master branch. -See the GitHub documentation on +changes, and creating a pull request when you're done; the development +integration branch is `dev`. See the GitHub documentation on [creating branches](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/) and [using pull requests](https://help.github.com/articles/using-pull-requests/). @@ -133,10 +133,9 @@ unfortunately this other algorithm is *also* called GitHub-flavoured markdown. In your markdown, please don't use tab characters and avoid "bare" URLs. In a hyperlink, the link text and URL should be on the same line. While historically we didn't care about the line length in markdown texts -(and more often than not put the whole paragraph into one line), this is subject -to change anytime soon, with 80-character limit _recommendation_ -(which is softer than the limit for C/C++ code) imposed on everything -_except hyperlinks_ (because wrapping hyperlinks breaks the rendering). +(and more often than not put the whole paragraph into one line), this is no more +recommended; instead, try to use 80-character limit (similar to the limit for +C/C++ code) _except hyperlinks_ - wrapping breaks them. Do not use trailing two spaces for line breaks, since these cannot be seen and may be silently removed by some tools. If, for whatever reason, a blank line @@ -155,14 +154,8 @@ just don't bankrupt us with it. Refactoring is welcome. ### Code style and formatting -As of Quotient 0.6, the C++ standard for newly written code is C++17 with C++20 -compatibility and a few restrictions, notably: -* standard library's _deduction guides_ cannot be used to lighten up syntax - in template instantiation, i.e. you have to still write - `std::array { 1, 2 }` instead of `std::array { 1, 2 }` (or use - `Quotient::make_array` helper from `util.h`), use `std::make_pair` to create - pairs etc. - once we move over to the later Apple toolchain, this will be - no more necessary; +As of Quotient 0.7, the C++ standard for newly written code is C++20 with a few +restrictions, notably: * enumerators and slots cannot have `[[attributes]]` because moc from Qt 5.12 chokes on them - this will be lifted when we move on to Qt 5.13 for the oldest supported version, in the meantime use `Q_DECL_DEPRECATED` and similar Qt @@ -176,11 +169,13 @@ accepted in PRs; however, unless explicitly marked with `// clang-format off` and `// clang-format on`, these deviations will be rectified any commit soon after. -Additional considerations: +Notable things from .clang-format: * 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - thank you for fixing it. * Prefer keeping lines within 80 characters. Slight overflows are ok only if that helps readability. + +Additionally: * Please don't make "hypocritical structs" with protected or private members. In general, `struct` is used to denote a plain-old-data structure, rather than data+behaviour. If you need access control or are adding yet another @@ -195,25 +190,25 @@ Additional considerations: * `std::array` and `std::deque` have no direct counterparts in Qt. * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. Examples of that - are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but - see the next point and also consider if you can supply a reasonable - copy/default constructor. + are `SyncRoomData` and `EventsArray<>`. Use STL containers for structures + having those but see the next point and also consider if you can supply + a reasonable copy/default constructor. * STL containers can be freely used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up code where you don't need COW, or when dealing with uncopyable data structures (see the previous point). However, exposing STL containers in the API is not encouraged (except where absolutely necessary, e.g. we use - `std::deque` for a timeline). Exposing STL containers or iterators in API - intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work - and therefore unlikely to be accepted into `master`. - * Prefer using `std::unique_ptr<>` over `QScopedPointer<>` as it gives - stronger guarantees. Earlier revisions of this text recommended using - `QScopedPointer<>` because Qt Creator's debugger UI had a display helper - for it; it now has helpers for both. -* Use `QVector` instead of `QList` where possible - see the + `std::deque` for a timeline). Especially when it comes to API intended + for usage from QML (e.g. `Q_PROPERTY`), STL containers or iterators are + unlikely to work and therefore unlikely to be accepted into `dev`. + * Notwithstanding the above (you're not going to use them with QML anyway), + prefer `std::unique_ptr<>` over `QScopedPointer<>` as it gives stronger + guarantees. +* Always use `QVector` instead of `QList` unless Qt's own API uses it - see the [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) - for details. + for details. With Qt 6, these two become the same type matching what used + to be `QVector` in Qt 5. ### API conventions @@ -231,23 +226,24 @@ this may change eventually. ### Comments Whenever you add a new call to the library API that you expect to be used -from client code, you must supply a proper doc-comment along with the call. -Doxygen style is preferred; but Javadoc is acceptable too. Some parts are -not documented at all; adding doc-comments to them is highly encouraged. +from client code, make sure to supply a proper doc-comment along with the call. +Quotient uses the Doxygen style; some legacy code may use Javadoc style but it +is not encouraged any more. Some parts are not documented at all; +adding doc-comments to them is highly encouraged and is a great first-time +contribution. Use `\brief` for the summary, and follow with details after -an empty doc-comment line. +an empty doc-comment line, using `\param`, `\return` etc. as necessary. For in-code comments, the advice is as follows: * Don't restate what's happening in the code unless it's not really obvious. - We assume the readers to have at least some command of C++ and Qt. If your - code is not obvious, consider making it clearer itself before commenting. -* Both C++ and Qt still come with their arcane features and dark corners, + We assume the readers to have some command of C++ and Qt. If your code is + not obvious, consider making it clearer itself before commenting. +* That said, both C++ and Qt have their arcane features and dark corners, and we don't want to limit anybody who feels they have a case for - variable templates, raw literals, or use `std::as_const` to avoid container - detachment. Use your experience to figure what might be less well-known to - readers and comment such cases (references to web pages, Quotient wiki etc. - are very much ok, the previous bullet notwithstanding). + variadic templates, raw literals, and so on. Use your experience to figure + what might be less well-known to readers and comment such cases: leave + references to web pages, Quotient wiki etc. * Make sure to document not so much "what" but more "why" certain code is done the way it is. In the worst case, the logic of the code can be reverse-engineered; but you can almost never reverse-engineer the line of @@ -255,26 +251,43 @@ For in-code comments, the advice is as follows: ### Automated tests -There's no testing framework as of now; either Catch or Qt Test or both will -be used eventually. - -The `tests/` directory contains a command-line program, quotest, used for -automated functional testing. Any significant addition to the library API -should be accompanied by a respective test in quotest. To add a test you should: -- Add a new test to the `TestSuite` class (technically, each test is a private - slot and there are two macros, `TEST_DECL()` and `TEST_IMPL()`, that conceal - passing the testing handle in `thisTest` variable to the test method). -- Add test logic to the slot, using `FINISH_TEST` macro to assert the test - outcome and complete the test (`FINISH_TEST` contains `return`). ALL - (even failing) branches should conclude with a `FINISH_TEST` (or `FAIL_TEST` - that is a shortcut for a failing `FINISH_TEST`) invocation, unless you - intend to have a "DID NOT FINISH" message in the logs in certain conditions. +We gradually introduce autotests based on a combination of CTest and Qt Test +frameworks - see `autotests/` directory. There are very few of those, as we +have just started adding those to the new code (you guessed it; adding more +tests to the old code is very welcome). + +Aside from that, libQuotient comes with a command-line end-to-end test suite +called Quotest. Any significant addition to the library API should be +accompanied by a respective test in `autotests/` and/or in Quotest. + +To add a test to autotests: +- In a new .cpp file in `autotests/`, define a test class derived from + QObject with `private Q_SLOTS:` section having the member functions called + for testing. If you feel more comfortable using a header file to define + the class, feel free to do so. If you're new to Qt Test framework, use + existing tests as a guidance. +- Add a `quotient_add_test` macro call with your test to + `autotests/CMakeLists.txt` + +To add a test to Quotest: +- In `quotest.cpp`, add a new test to the `TestSuite` class. Similar to Qt Test, + each test in Quotest is a private slot; unlike Qt Test, you should use + special macros, `TEST_DECL()` and `TEST_IMPL()`, to declare and define + the test (those macros conceal passing the testing handle in `thisTest` + variable to the test method). +- In the test function definition, add test logic using `FINISH_TEST` macro + to check for the test outcome and complete the test (be mindful that + `FINISH_TEST` always `return`s, not only in case of error). ALL (even failing) + branches should conclude with a `FINISH_TEST` (or `FAIL_TEST` that is + a shortcut for a failing `FINISH_TEST`) invocation, unless you intend to have + a "DID NOT FINISH" message in the logs in certain conditions. The `TestManager` class sets up some basic test fixture to help you with testing; -notably, the tests can rely on having an initialised `Room` object for the test -room in `targetRoom` member variable. PRs to introduce a proper testing framework -are very welcome (make sure to migrate tests from quotest though). Note that -tests can go async, which is the biggest hurdle for Qt Test adoption. +notably, the tests can rely on having an initialised `Room` object with loaded +state for the test room in `targetRoom` member variable. Note that it's normal +for tests to go async, which is not something Qt Test is easy with (and this +is why Quotest doesn't directly use Qt Test but rather fetches a few ideas +from it). ### Security, privacy, and performance @@ -306,13 +319,15 @@ this trend. We want the software to have decent performance for users even on weaker machines. At the same time we keep libQuotient single-threaded as much as possible, to keep the code simple. That means being cautious about operation -complexity (read about big-O notation if you need a kickstart on the topic). +complexity (read about big-O notation if you need a kickstart on the subject). This especially refers to operations on the whole timeline and the list of users - each of these can have tens of thousands of elements so even operations with linear complexity, if heavy enough (with I/O or complex processing), can produce noticeable GUI freezing or stuttering. When you don't see a way -to reduce algorithmic complexity, embed occasional `processEvents()` invocations -in heavy loops (see `Connection::saveState()` to get the idea). +to reduce algorithmic complexity, either split processing into isolated +pieces that can be individually scheduled as queued events (see the end of +`Connection::consumeRoomData()` to get the idea) or uncouple the logic from +GUI and execute it outside of the main thread with `QtConcurrent` facilities. Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important @@ -323,14 +338,10 @@ that might not give the benefits you think it would. Speaking of profiling logs (see README.md on how to turn them on) - if you expect some code to take considerable (more than 10k "simple operations") time you might want to setup a `QElapsedTimer` and drop the elapsed time into logs -under `PROFILER` logging category (see the existing code for examples - -`room.cpp` has quite a few). In order to reduce small timespan logging spam, -`PROFILER` log lines are usually guarded by a check that the timer counted -some considerable time (200 microseconds by default, 20 microseconds for -tighter parts). It's possible to override this limit library-wide by passing -the new value (in microseconds) in `PROFILER_LOG_USECS` definition to -the compiler; I don't think anybody ever used this facility. If you used it, -and are reading this text - let me (`@kitsune`) know. +under `PROFILER` logging category. See the existing code for examples - +`room.cpp` has quite a few. In order to reduce small timespan logging spam, +`PROFILER` log lines are usually guarded by a check that the timer counted big +enough time (200 microseconds by default, 20 microseconds for tighter parts). ### Generated C++ code for CS API The code in `lib/csapi`, `lib/identity` and `lib/application-service`, although @@ -338,9 +349,9 @@ it resides in Git, is actually generated from the official Swagger/OpenAPI definition files. If you're unhappy with something in there and want to improve the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling -the below text can be found in .travis.yml (our CI configuration actually -regenerates those files upon every build). As described below, there is also -a handy build target for CMake. +the below text can be found in .github/workflows/ci.yml (our CI configuration +tests regeneration of those files). As described below, there is also a handy +build target for CMake. #### Why generate the code at all? Because otherwise we have to do monkey business of writing boilerplate code, @@ -353,63 +364,71 @@ found in [this talk about API description languages](https://youtu.be/W5TmRozH-r that also briefly touches on GTAD. #### Prerequisites for CS API code generation -1. Get the source code of GTAD and its dependencies, e.g. using the command: - `git clone --recursive https://github.com/KitsuneRal/gtad.git` -2. Build GTAD: in the source code directory, do `cmake . && cmake --build .` - (you might need to pass `-DCMAKE_PREFIX_PATH=`, - similar to libQuotient itself). -3. Get the Matrix CS API definitions that are included in a matrix-doc repo. - You can `git clone https://github.com/matrix-org/matrix-doc.git`, - the official repo; it's recommended though to instead - `git clone https://github.com/quotient-im/matrix-doc.git` - this repo closely - follows the official one, with an additional guarantee that you can always - generate working Quotient code from its HEAD commit. And of course you - can use your own repository if you need to change the API definition. -4. If you plan to submit a PR or just would like the generated code to be - properly formatted, you should either ensure you have clang-format - (version 6 at least) in your PATH or pass the _absolute_ path to it by adding - `-DCLANG_FORMAT=` to the CMake invocation below. +1. Get the source code of GTAD and its dependencies. Recent libQuotient + includes GTAD as a submodule so you can get everything you need by updating + gtad/gtad submodule in libQuotient sources: + `git submodule update --init --recursive gtad/gtad`. + + You can also just clone GTAD sources to keep them separate from libQuotient: + `git clone --recursive https://github.com/quotient-im/gtad.git` +2. Configure and build GTAD: same as libQuotient, it uses CMake so this should + be quite straightforward (if not - you're probably not quite ready for this + stuff anyway). +3. Get Matrix CS API definitions from a matrix-spec repo. Although the official + repo is at https://github.com/matrix-org/matrix-spec.git` (formerly + https://github.com/matrix-org/matrix-doc.git), you may or may not be able + to generate working code from it because the way it evolves is not + necessarily in line with libQuotient needs. For that reason, a soft fork + of the official definitions is kept at + https://github.com/quotient-im/matrix-spec.git that guarantees buildability + of the generated code. This repo closely follows the official one (but maybe + not its freshest commit), applying a few adjustments on top. And of course + you can use your own repository if you need to change the API definition. +4. If you plan to submit a PR with the generated code to libQuotient or just + would like it to be properly formatted, you should either ensure you have + clang-format (version 10 at least) in your PATH or pass + `-DCLANG_FORMAT=` to CMake, as mentioned in the next section. #### Generating CS API contents 1. Pass additional configuration to CMake when configuring libQuotient: - `-DMATRIX_DOC_PATH= -DGTAD_PATH=`. - If everything's right, these two CMake variables will be mentioned in - CMake output and will trigger configuration of an additional build target, - see the next step. -2. Generate the code: `cmake --build --target update-api`; - if you use CMake with GNU Make, you can just do `make update-api` instead. - Building this target will create (overwriting without warning) `.h` and `.cpp` - files in `lib/csapi`, `lib/identity`, `lib/application-service` for all - YAML files it can find in `matrix-doc/api/client-server` and other files - in `matrix-doc/api` these depend on. -3. Re-run CMake so that the build system knows about new files, if there are any - (this step is unnecessary if you use CMake 3.12 or later). + `-DMATRIX_SPEC_PATH=/path/to/matrix-spec/ -DGTAD_PATH=/path/to/gtad`. + Note that `MATRIX_SPEC_PATH` should lead to the repo while `GTAD_PATH` should + have the path to GTAD binary. If you need to specify where your clang-format + is (see the previous section) add `-DCLANG_FORMAT=/path/to/clang-format` to + the line above. If everything's right, the detected locations will be + mentioned in CMake output and will trigger configuration of an additional + build target called `update-api`. +2. Generate the code: `cmake --build --target update-api`. + Building this target will create (overwriting without warning) source files + in `lib/csapi`, `lib/identity`, `lib/application-service` for all YAML files + it can find in `/path/to/matrix-spec/data/api/client-server` and their + dependencies. #### Changing generated code See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQuotient are described here. GTAD uses the following three kinds of sources: -1. OpenAPI files. Each file is treated as a separate source (if you worked with - swagger-codegen - you do _not_ need to have a single file for the whole API). -2. A configuration file, in our case it's `gtad/gtad.yaml` - this one is common - for all OpenAPI files GTAD is invoked on. +1. OpenAPI files. Each file is treated as a separate source (unlike + swagger-codegen, you do _not_ need to have a single file for the whole API). +2. A configuration file, in Quotient case it's `gtad/gtad.yaml` - common for + all OpenAPI files GTAD is invoked on. 3. Source code template files: `gtad/*.mustache` - are also common. The Mustache files have a templated (not in C++ sense) definition of a network -job, deriving from BaseJob; if necessary, data structure definitions used +job class derived from BaseJob; if necessary, data structure definitions used by this job are put before the job class. Bigger Mustache files look a bit hideous for a newcomer; and the only known highlighter that can handle -the combination of Mustache (originally a web templating language) and C++ is -provided in CLion IDE. Fortunately, all our Mustache files are reasonably +the combination of Mustache (originally a web templating language) and C++ can +be found in CLion IDE. Fortunately, all our Mustache files are reasonably concise and well-formatted these days. To simplify things some reusable Mustache blocks are defined in `gtad.yaml` - -see its `mustache:` section. Adventurous souls that would like to figure +see its `mustache:` section. Adventurous souls that would like to figure what's going on in these files should speak up in the Quotient room - I (Kitsune) will be very glad to help you out. The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": * `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible. -* `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around. +* `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). * `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h`, a drop-in upgrade over `std::optional`. * `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). * `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. @@ -448,17 +467,13 @@ the assumed code style from your shoulders (and fingers) to your computer. ### Other tools Recent versions of Qt Creator and CLion can automatically run your code through -clang-tidy. The following list of clang-tidy checks gives a good insight -without too many false positives: -`-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-constructor,bugprone-string-integer-assignment,bugprone-suspicious-*,bugprone-terminating-continue,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl03-c,cert-dcl21-cpp,cert-dcl50-cpp,cert-dcl54-cpp,cert-dcl58-cpp,cert-env33-c,cert-err09-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-err61-cpp,cert-fio38-c,cert-flp30-c,cert-msc30-c,cert-msc50-cpp,cert-oop11-cpp,clang-analyzer-apiModeling.StdCLibraryFunctions,clang-analyzer-core.CallAndMessage,clang-analyzer-core.NullDereference,clang-analyzer-cplusplus.*,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-non-private-member-variables-in-classes,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-slicing,hicpp-deprecated-headers,hicpp-invalid-access-moved,hicpp-member-init,hicpp-move-const-arg,hicpp-new-delete-operators,hicpp-static-assert,hicpp-undelegated-constructor,hicpp-use-*,misc-*,-misc-definitions-in-headers,-misc-no-recursion,-misc-non-private-member-variables-in-classes,modernize-loop-convert,modernize-pass-by-value,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-*,-modernize-use-trailing-return-type,performance-*,-performance-no-automatic-move,-performance-noexcept-move-constructor,-performance-unnecessary-*,readability-*,-readability-braces-around-statements,-readability-implicit-bool-conversion,-readability-isolate-declaration,-readability-magic-numbers,-readability-named-parameter,-readability-qualified-auto`. - -Qt Creator, in addition, knows about clazy, an even deeper Qt-aware static -analysis tool that produces some notices about Qt-specific issues that are -easy to overlook otherwise, such as possible unintended copying of -a Qt container, or unguarded null pointers. You can use this time to time -(see Analyze menu in Qt Creator) instead of hogging your machine with -deep analysis as you type (or after each saving, depending on your version -of Qt Creator). Most of clazy checks are relevant to our code, except: +clang-tidy. The source code contains `.clang-tidy` file with the recommended +set of checks that doesn't give too many false positives. + +Qt Creator in addition knows about clazy, a Qt-aware static analysis tool that +hunts for Qt-specific issues that are easy to overlook otherwise, such as +possible unintended copying of a Qt container. Most of clazy checks are relevant +to our code, except: `fully-qualified-moc-types,overloaded-signal,qstring-comparison-to-implicit-char,foreach,non-pod-global-static,qstring-allocations,jni-signatures,qt4-qstring-from-array`. ### Submitting API changes @@ -466,7 +481,7 @@ of Qt Creator). Most of clazy checks are relevant to our code, except: If you changed the API definitions, the path to upstream becomes somewhat intricate, as you have to coordinate with two projects, making up to 4 PRs along the way. The recommended sequence depends on whether or not you have to -[write an Matrix Spec Change aka MSC](https://matrix.org/docs/spec/proposals). +[write a Matrix Spec Change aka MSC](https://matrix.org/docs/spec/proposals). Usually you have to, unless your API changes keep API semantics intact. In that case: 1. Submit an MSC before submitting changes to the API definition files and @@ -475,22 +490,22 @@ In that case: but it's necessary for the Matrix ecosystem integrity. 3. When your MSC has at least some approvals (not necessarily a complete acceptance but at least some approvals should be there) submit a PR to - libQuotient, referring to your `matrix-doc` repo. Make sure that generated + libQuotient, referring to your `matrix-spec` repo. Make sure that generated files are committed separately from non-generated ones (no need to make two PRs; just separate them in different commits). -4. If your libQuotient PR is approved and MSC is not there yet you'll be asked - to submit a PR with API definition files at - `https://github.com/quotient-im/matrix-doc`. Note that this is _not_ +4. If/when your libQuotient PR is approved and MSC is not there yet you'll + be asked to submit a PR with API definition files at + `https://github.com/quotient-im/matrix-spec`. Note that this is _not_ an official repo; but you can refer to your libQuotient PR as an _implementation_ of the MSC - a necessary step before making a so-called "spec PR". -5. Once MSC is accepted, submit your `matrix-doc` changes as a PR to - `https://github.com/matrix-org/matrix-doc` (the "spec PR" mentioned above). +5. Once MSC is accepted, submit your `matrix-spec` changes as a PR to + `https://github.com/matrix-org/matrix-spec` (the "spec PR" mentioned above). This will require that your submission meets the standards set by this project (they are quite reasonable and not too hard to meet). If your changes don't need an MSC, it becomes a more straightforward combination -of 2 PRs: one to `https://github.com/matrix-org/matrix-doc` ("spec PR") and one +of 2 PRs: one to `https://github.com/matrix-org/matrix-spec` ("spec PR") and one to libQuotient (with the same guidance about putting generated and non-generated files in different commits). @@ -512,26 +527,23 @@ When writing git commit messages, try to follow the guidelines in C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very conservative about adding dependencies to libQuotient. That relates to additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. -Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for futures and automated testing, so PRs onboarding those will be considered with much gratitude. - Some cases need additional explanation: * Before rolling out your own super-optimised container or algorithm written from scratch, take a good long look through documentation on Qt and C++ standard library. Please try to reuse the existing facilities as much as possible. -* You should have a good reason (or better several ones) to add a component - from KDE Frameworks. We don't rule this out and there's no prejudice against - KDE; it just so happened that KDE Frameworks is one of most obvious - reuse candidates but so far none of these components survived - as libQuotient deps. So we are cautious. Extra notice to KDE folks: - I'll be happy if an addon library on top of libQuotient is made using - KDE facilities, and I'm willing to take part in its evolution; but please - also respect LXDE people who normally don't have KDE frameworks installed. +* libQuotient is a library to build Qt applications; for that reason, + components from KDE Frameworks should be really lightweight and useful + to be accepted as a dependency. If the intention is to better integrate + libQuotient into KDE environment there's nothing wrong in building another + library on top of libQuotient. Consider people who run LXDE and normally + don't have KDE frameworks installed (some even oppose installing those) - + libQuotient caters to them too. * Never forget that libQuotient is aimed to be a non-visual library; QtGui in dependencies is only driven by (entirely offscreen) dealing with QImages. While there's a bunch of visual code (in C++ and QML) shared between Quotient-enabled _applications_, this is likely to end up - in a separate (Quotient-enabled) library, rather than libQuotient itself. + in a separate (Quotient-backed) library, rather than libQuotient itself. ## Attribution diff --git a/README.md b/README.md index 15f1fcd7..e42cb488 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ and bundle it with your application. - Qt 5 (either Open Source or Commercial), 5.12 or higher - CMake 3.16 or newer (from your package management system or [the official website](https://cmake.org/download/)) -- A C++ toolchain with complete (as much as possible) C++17 and basic C++20: +- A C++ toolchain with that supports at least some subset of C++20: - GCC 10 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) and Visual Studio 2019 (Windows) are the oldest officially supported. - Any build system that works with CMake should be fine: -- cgit v1.2.3 From fcf0153d8304700733ea8e74f5ed3125c2a959e1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 13 Jun 2022 20:51:27 +0200 Subject: Add .clang-tidy file [skip ci] --- .clang-tidy | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..07cef37f --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,179 @@ +--- +Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-macro-usage,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: file +CheckOptions: + - key: bugprone-argument-comment.IgnoreSingleArgument + value: '1' + - key: bugprone-argument-comment.StrictMode + value: '1' + - key: bugprone-assert-side-effect.AssertMacros + value: assert,NSAssert,NSCAssert,Q_ASSERT,Q_ASSERT_X + - key: bugprone-assert-side-effect.CheckFunctionCalls + value: 'true' +# - key: bugprone-dangling-handle.HandleClasses +# value: 'std::basic_string_view;std::experimental::basic_string_view' +# - key: bugprone-signed-char-misuse.CharTypdefsToIgnore +# value: '' +# - key: bugprone-signed-char-misuse.DiagnoseSignedUnsignedCharComparisons +# value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression + value: 'true' + - key: bugprone-string-constructor.LargeLengthThreshold + value: '8388608' + - key: bugprone-string-constructor.StringNames + value: '::std::basic_string;::std::basic_string_view' + - key: bugprone-string-constructor.WarnOnLargeLength + value: 'true' +# - key: bugprone-suspicious-enum-usage.StrictMode +# value: 'false' +# - key: bugprone-suspicious-include.HeaderFileExtensions +# value: ';h;hh;hpp;hxx' +# - key: bugprone-suspicious-include.ImplementationFileExtensions +# value: 'c;cc;cpp;cxx' +# - key: bugprone-suspicious-missing-comma.SizeThreshold +# value: '5' +# - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison +# value: 'false' +# - key: bugprone-suspicious-string-compare.StringCompareLikeFunctions +# value: '' +# - key: bugprone-too-small-loop-variable.MagnitudeBitsUpperLimit +# value: '16' +# - key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField +# value: 'true' +# - key: cert-dcl59-cpp.HeaderFileExtensions +# value: ';h;hh;hpp;hxx' +# - key: cert-msc32-c.DisallowedSeedTypes +# value: 'time_t,std::time_t' +# - key: cert-msc51-cpp.DisallowedSeedTypes +# value: 'time_t,std::time_t' + - key: cppcoreguidelines-macro-usage.AllowedRegexp + value: '' + - key: cppcoreguidelines-macro-usage.CheckCapsOnly + value: 'false' + - key: cppcoreguidelines-narrowing-conversions.IgnoreConversionFromTypes + value: 'size_t;ptrdiff_t;size_type;difference_type' +# - key: cppcoreguidelines-narrowing-conversions.PedanticMode +# value: 'false' +# - key: cppcoreguidelines-narrowing-conversions.WarnOnEquivalentBitWidth +# value: 'true' +# - key: cppcoreguidelines-narrowing-conversions.WarnOnIntegerToFloatingPointNarrowingConversion +# value: 'true' + - key: cppcoreguidelines-narrowing-conversions.WarnWithinTemplateInstantiation + value: 'true' +# - key: cppcoreguidelines-pro-type-member-init.UseAssignment +# value: 'false' +# - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted +# value: 'false' +# - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor +# value: 'false' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '1' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '25' +# - key: misc-definitions-in-headers.HeaderFileExtensions +# value: ';h;hh;hpp;hxx' + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' +# - key: misc-non-private-member-variables-in-classes.IgnorePublicMemberVariables +# value: 'false' +# - key: modernize-loop-convert.MakeReverseRangeFunction +# value: '' +# - key: modernize-loop-convert.MakeReverseRangeHeader +# value: '' +# - key: modernize-loop-convert.MaxCopySize +# value: '16' +# - key: modernize-loop-convert.NamingStyle +# value: CamelCase +# - key: modernize-loop-convert.UseCxx20ReverseRanges +# value: 'true' +# - key: modernize-make-shared.IgnoreMacros +# value: 'true' +# - key: modernize-make-shared.IgnoreDefaultInitialization +# value: 'true' +# - key: modernize-make-unique.IgnoreMacros +# value: 'true' +# - key: modernize-make-unique.IgnoreDefaultInitialization +# value: 'true' + - key: modernize-use-auto.MinTypeNameLength + value: '0' +# - key: modernize-use-auto.RemoveStars +# value: 'false' +# - key: modernize-use-bool-literals.IgnoreMacros +# value: 'true' +# - key: modernize-use-default-member-init.IgnoreMacros +# value: 'true' + - key: modernize-use-default-member-init.UseAssignment + value: 'true' +# - key: modernize-use-emplace.SmartPointers +# value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: modernize-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' +# - key: modernize-use-emplace.TupleTypes +# value: '::std::pair;::std::tuple' +# - key: modernize-use-equals-default.IgnoreMacros +# value: 'true' +# - key: modernize-use-equals-delete.IgnoreMacros +# value: 'true' +# - key: modernize-use-noexcept.UseNoexceptFalse +# value: 'true' +# - key: modernize-use-using.IgnoreMacros +# value: 'true' + - key: modernize-raw-string-literal.DelimiterStem + value: '' +# - key: modernize-raw-string-literal.ReplaceShorterLiterals +# value: 'false' +# - key: performance-faster-string-find.StringLikeClasses +# value: '::std::basic_string;::std::basic_string_view' +# - key: performance-for-range-copy.AllowedTypes +# value: '' +# - key: performance-for-range-copy.WarnOnAllAutoCopies +# value: 'false' +# - key: performance-inefficient-string-concatenation.StrictMode +# value: 'false' + - key: performance-inefficient-vector-operation.VectorLikeClasses + value: '::std::vector,QVector,::std::deque' +# - key: performance-unnecessary-copy-initialization.AllowedTypes +# value: '' + - key: readability-else-after-return.WarnOnConditionVariables + value: 'true' +# - key: readability-else-after-return.WarnOnUnfixable +# value: 'true' +# - key: readability-function-size.StatementThreshold +# value: '800' +# - key: readability-function-cognitive-complexity.DescribeBasicIncrements +# value: 'true' +# - key: readability-function-cognitive-complexity.IgnoreMacros +# value: 'true' +# - key: readability-function-cognitive-complexity.Threshold +# value: '25' +# - key: readability-implicit-bool-conversion.AllowIntegerConditions +# value: 'false' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: 'true' +# - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros +# value: 'true' + - key: readability-inconsistent-declaration-parameter-name.Strict + value: 'true' +# - key: readability-qualified-auto.AddConstToQualified +# value: 'true' +# - key: readability-redundant-declaration.IgnoreMacros +# value: 'true' +# - key: readability-redundant-member-init.IgnoreBaseInCopyConstructors +# value: 'false' +# - key: readability-redundant-smartptr-get.IgnoreMacros +# value: 'true' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: 'true' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: 'true' +# - key: readability-uniqueptr-delete-release.PreferResetCall +# value: 'false' +# - key: readability-uppercase-literal-suffix.IgnoreMacros +# value: 'true' + - key: readability-uppercase-literal-suffix.NewSuffixes + value: 'f;F' +... + -- cgit v1.2.3 From cc69883ef7219ec42cb7bdb2e3d66256c17a6532 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 14 Jun 2022 10:05:53 +0200 Subject: Stop using LGTM.com Its platform has been lagging behind most of the time; but more importantly, the value from its analysis is almost non-existent, with just one considerable issue being identified over the recent year if not more. These days we have clang-tidy and Sonar that are much better at static code analysis. [skip ci] --- .lgtm.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .lgtm.yml diff --git a/.lgtm.yml b/.lgtm.yml deleted file mode 100644 index 9cf3583d..00000000 --- a/.lgtm.yml +++ /dev/null @@ -1,18 +0,0 @@ -path_classifiers: - library: - - 3rdparty/* - test: - - exclude: tests/quotest.cpp # Let alerts from this come up too -extraction: - cpp: - prepare: - packages: # Assuming package base of eoan - - qtmultimedia5-dev -# after_prepare: -# - git clone https://gitlab.matrix.org/matrix-org/olm.git -# - pushd olm -# - cmake . -Bbuild -GNinja -# - cmake --build build -# - popd - configure: - command: "CXX=clang++-12 cmake . -GNinja" # -DOlm_DIR=olm/build" -- cgit v1.2.3 From a4f0071395939a93bcb3afd72085415a25216701 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 14 Jun 2022 10:28:51 +0200 Subject: CI: bump used versions for GitHub Actions Also, use MSVC 2019 on Windows. --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 172c027f..212273bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,12 @@ jobs: compiler: MSVC platform: x64 qt-version: '5.12.12' - qt-arch: win64_msvc2017_64 + qt-arch: win64_msvc2019_64 - os: windows-2019 compiler: MSVC platform: x64 qt-version: '5.12.12' - qt-arch: win64_msvc2017_64 + qt-arch: win64_msvc2019_64 update-api: update-api env: @@ -72,7 +72,7 @@ jobs: key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache - name: Install Qt - uses: jurplel/install-qt-action@v2.11.1 + uses: jurplel/install-qt-action@v2.14.0 with: version: ${{ matrix.qt-version }} arch: ${{ matrix.qt-arch }} @@ -192,7 +192,7 @@ jobs: - name: Initialize CodeQL tools if: env.CODEQL_ANALYSIS - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: cpp # If you wish to specify custom queries, you can do so here or in a config file. @@ -235,7 +235,7 @@ jobs: - name: Perform CodeQL analysis if: env.CODEQL_ANALYSIS - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 - name: Run sonar-scanner if: matrix.sonar != '' -- cgit v1.2.3 From b55c110ecbca5ad41ac9ccb5647836709ac8f4a8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 14 Jun 2022 11:40:34 +0200 Subject: CI: Switch to Qt 5.15 and introduce Qt 6 options Qt 6 builds are allowed to fail for now. --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 212273bd..d46cfb6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,14 +14,14 @@ concurrency: ci-${{ github.ref }} jobs: CI: runs-on: ${{ matrix.os }} - continue-on-error: false + continue-on-error: ${{ matrix.qt-version != '5.15.2' }} # Qt 6 will fail for now strategy: fail-fast: false max-parallel: 1 matrix: os: [ ubuntu-20.04, macos-10.15 ] compiler: [ Clang ] # GCC builds are added individually below - qt-version: [ '5.12.12' ] + qt-version: [ '5.15.2', '6.3.1' ] # Not using binary values here, to make the job captions more readable e2ee: [ '', e2ee ] update-api: [ '', update-api ] @@ -29,6 +29,8 @@ jobs: platform: [ '' ] qt-arch: [ '' ] exclude: + - qt-version: '6.3.1' + update-api: update-api - os: windows-2019 e2ee: e2ee # Not supported by the current CI script - os: macos-10.15 @@ -36,23 +38,23 @@ jobs: include: - os: ubuntu-latest compiler: GCC - qt-version: '5.12.12' + qt-version: '5.15.2' e2ee: e2ee sonar: sonar - os: ubuntu-20.04 compiler: GCC - qt-version: '5.12.12' + qt-version: '5.15.2' e2ee: e2ee update-api: update-api - os: windows-2019 compiler: MSVC platform: x64 - qt-version: '5.12.12' + qt-version: '5.15.2' qt-arch: win64_msvc2019_64 - os: windows-2019 compiler: MSVC platform: x64 - qt-version: '5.12.12' + qt-version: '5.15.2' qt-arch: win64_msvc2019_64 update-api: update-api -- cgit v1.2.3 From da5156f5e2da08123549b554d9eafcf366fa4e11 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 14 Jun 2022 18:13:39 +0200 Subject: Rearrange CI jobs to spend time more efficiently - CodeQL analysis was executed on every job that ran Clang, humping the total execution time by 10+ minutes alone. Now it only runs on a single job. - libolm is no more compiled but installed from the repo, along with libssl-dev; and both are installed in the same transaction as ninja and valgrind, shaving out one apt transaction - One more Windows job has been added to test building with Qt 6.3.1 on that OS. - Qt version is pushed earlier in the job matrix, as it becomes more significant than the compiler for a given platform. --- .github/workflows/ci.yml | 144 +++++++++++++++++++++++------------------------ 1 file changed, 71 insertions(+), 73 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d46cfb6c..ab581238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,43 +20,59 @@ jobs: max-parallel: 1 matrix: os: [ ubuntu-20.04, macos-10.15 ] - compiler: [ Clang ] # GCC builds are added individually below qt-version: [ '5.15.2', '6.3.1' ] + compiler: [ LLVM ] # Not using binary values here, to make the job captions more readable e2ee: [ '', e2ee ] update-api: [ '', update-api ] - sonar: [ '' ] + static-analysis: [ '' ] platform: [ '' ] qt-arch: [ '' ] exclude: - qt-version: '6.3.1' - update-api: update-api - - os: windows-2019 - e2ee: e2ee # Not supported by the current CI script + update-api: update-api # Generated code is not specific to Qt version + - os: ubuntu-20.04 + e2ee: e2ee # Will be re-added with static analysis below + # TODO: Enable E2EE on Windows and macOS - os: macos-10.15 - e2ee: e2ee # Missing OpenSSL + e2ee: e2ee include: + - os: windows-2019 + qt-version: '5.15.2' + compiler: MSVC + platform: x64 + qt-arch: win64_msvc2019_64 + - os: ubuntu-20.04 + qt-version: '5.15.2' + compiler: LLVM + e2ee: e2ee + static-analysis: codeql - os: ubuntu-latest - compiler: GCC qt-version: '5.15.2' + compiler: GCC e2ee: e2ee - sonar: sonar + static-analysis: sonar - os: ubuntu-20.04 - compiler: GCC qt-version: '5.15.2' + compiler: GCC e2ee: e2ee update-api: update-api + - os: ubuntu-20.04 + qt-version: '5.15.2' + compiler: LLVM + update-api: update-api - os: windows-2019 + qt-version: '6.3.1' compiler: MSVC + # e2ee: e2ee # TODO platform: x64 - qt-version: '5.15.2' qt-arch: win64_msvc2019_64 - os: windows-2019 + qt-version: '5.15.2' compiler: MSVC + update-api: update-api platform: x64 - qt-version: '5.15.2' qt-arch: win64_msvc2019_64 - update-api: update-api env: SONAR_SERVER_URL: 'https://sonarcloud.io' @@ -66,44 +82,14 @@ jobs: with: fetch-depth: 0 - - name: Cache Qt - id: cache-qt - uses: actions/cache@v2 - with: - path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache - - - name: Install Qt - uses: jurplel/install-qt-action@v2.14.0 - with: - version: ${{ matrix.qt-version }} - arch: ${{ matrix.qt-arch }} - cached: ${{ steps.cache-qt.outputs.cache-hit }} - - - name: Install Ninja (macOS/Windows) - if: ${{ !startsWith(matrix.os, 'ubuntu') }} - uses: seanmiddleditch/gha-setup-ninja@v3 - - - name: Install Ninja and Valgrind (Linux) - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get -qq install ninja-build valgrind - echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV - - name: Setup build environment run: | - if [ "${{ matrix.compiler }}" == "GCC" ]; then - CXX_VERSION_POSTFIX='-10' - echo "CC=gcc$CXX_VERSION_POSTFIX" >>$GITHUB_ENV - echo "CXX=g++$CXX_VERSION_POSTFIX" >>$GITHUB_ENV - elif [[ '${{ matrix.compiler }}' == 'Clang' ]]; then - if [[ '${{ runner.os }}' == 'Linux' ]]; then - CXX_VERSION_POSTFIX='-11' - # Do CodeQL analysis on one of Linux branches - echo "CODEQL_ANALYSIS=true" >>$GITHUB_ENV - fi - echo "CC=clang$CXX_VERSION_POSTFIX" >>$GITHUB_ENV - echo "CXX=clang++$CXX_VERSION_POSTFIX" >>$GITHUB_ENV + if [ '${{ matrix.compiler }}' == 'GCC' ]; then + echo "CC=gcc-10" >>$GITHUB_ENV + echo "CXX=g++-10" >>$GITHUB_ENV + elif [[ '${{ runner.os }}' != 'Windows' ]]; then + echo "CC=clang" >>$GITHUB_ENV + echo "CXX=clang++" >>$GITHUB_ENV fi if grep -q 'refs/tags' <<<'${{ github.ref }}'; then VERSION="$(git describe --tags)" @@ -112,20 +98,18 @@ jobs: else VERSION="$(git describe --all --contains)-ci${{ github.run_number }}-$(git rev-parse --short HEAD)" fi - echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - # Build libQuotient as a shared library across platforms but also - # check the static configuration somewhere + echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/Qt-${{ matrix.qt-version }}/${{ matrix.compiler }}" >>$GITHUB_ENV + CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DBUILD_SHARED_LIBS=${{ runner.os == 'Linux' }} \ -DCMAKE_INSTALL_PREFIX=~/.local \ -DCMAKE_PREFIX_PATH=~/.local \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" - if [ -n "${{ matrix.sonar }}" ]; then + if [ '${{ matrix.static-analysis }}' == 'sonar' ]; then mkdir -p $HOME/.sonar CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CXX_FLAGS=--coverage" - echo "COV=gcov$CXX_VERSION_POSTFIX" >>$GITHUB_ENV fi echo "CMAKE_ARGS=$CMAKE_ARGS" >>$GITHUB_ENV @@ -139,14 +123,42 @@ jobs: cmake -E make_directory ${{ runner.workspace }}/build echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV - - name: Setup MSVC environment + - name: Cache Qt + id: cache-qt + uses: actions/cache@v2 + with: + path: ${{ runner.workspace }}/Qt + key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache + + - name: Install Qt + uses: jurplel/install-qt-action@v2.14.0 + with: + version: ${{ matrix.qt-version }} + arch: ${{ matrix.qt-arch }} + cached: ${{ steps.cache-qt.outputs.cache-hit }} + + - name: Install Ninja (macOS/Windows) + if: ${{ !startsWith(matrix.os, 'ubuntu') }} + uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Install dependencies (Linux) + if: startsWith(matrix.os, 'ubuntu') + run: | + if [ -n "${{ matrix.e2ee }}" ]; then + EXTRA_DEPS="libssl-dev libolm-dev" + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV + fi + sudo apt-get -qq install ninja-build valgrind $EXTRA_DEPS + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV + + - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 if: matrix.compiler == 'MSVC' with: arch: ${{ matrix.platform }} - name: Download and set up Sonar Cloud tools - if: matrix.sonar != '' + if: matrix.static-analysis == 'sonar' env: SONAR_SCANNER_VERSION: 4.6.2.2472 run: | @@ -159,20 +171,6 @@ jobs: unzip -o sonar-scanner-cli*.zip popd - - name: Install OpenSSL - if: ${{ contains(matrix.os, 'ubuntu') && matrix.e2ee }} - run: | - sudo apt-get install libssl-dev - - - name: Build and install olm - if: matrix.e2ee - working-directory: ${{ runner.workspace }} - run: | - git clone https://gitlab.matrix.org/matrix-org/olm.git - cmake -S olm -B build/olm $CMAKE_ARGS - cmake --build build/olm --target install - echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - - name: Build and install QtKeychain run: | cd .. @@ -193,7 +191,7 @@ jobs: echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV - name: Initialize CodeQL tools - if: env.CODEQL_ANALYSIS + if: matrix.static-analysis == 'codeql' uses: github/codeql-action/init@v2 with: languages: cpp @@ -236,18 +234,18 @@ jobs: timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually - name: Perform CodeQL analysis - if: env.CODEQL_ANALYSIS + if: matrix.static-analysis == 'codeql' uses: github/codeql-action/analyze@v2 - name: Run sonar-scanner - if: matrix.sonar != '' + if: matrix.static-analysis == 'sonar' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | mkdir .coverage && pushd .coverage find $BUILD_PATH -name '*.gcda' -print0 \ - | xargs -0 $COV -s $GITHUB_WORKSPACE -pr + | xargs -0 gcov -s $GITHUB_WORKSPACE -pr # Coverage of the test source code is not tracked, as it is always 100% # (if not, some tests failed and broke the build at an earlier stage) rm -f quotest* autotests* -- cgit v1.2.3 From f779b235ddac990d17a9a8d8dd222b9e0e7abd49 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 17 Jun 2022 10:35:22 +0200 Subject: Make Connection::sendToDevices() an actual slot Although Qt 5 didn't complain about that, you could never really use sendToDevices() in its slot (or even invocable) capacity because Qt's meta-type system could not handle move-only UsersToDevicesToEvents. Qt 6 is more stringent; the build fails at trying to instantiate QMetaType for that type (with a rather unhelpful error message thrown by Clang, and more helpful but very verbose diagnostic from MSVC) because it does not provide a copy constructor. However, sendToDevice doesn't really need to have full-blown events in that parameter; just the content of the event is equally fine. This commit does exactly that: replaces UsersToDevicesToEvents with UsersToDevicesToContent that contains QJsonObject's instead of EventPtr's. The code around is updated accordingly. Also: factor out the key event JSON creation from makeMessageEventForSessionKey() because it's the same JSON for each target device; the function therefore is called encryptSessionKeyEvent() now. --- lib/connection.cpp | 74 +++++++++++++++++++++++------------------------------- lib/connection.h | 5 ++-- 2 files changed, 34 insertions(+), 45 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 101bef89..3e44513b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -381,10 +381,9 @@ public: const QString& device) const; QString edKeyForUserDevice(const QString& userId, const QString& device) const; - std::unique_ptr makeEventForSessionKey( - const QString& roomId, const QString& targetUserId, - const QString& targetDeviceId, const QByteArray& sessionId, - const QByteArray& sessionKey) const; + QJsonObject encryptSessionKeyEvent(QJsonObject payloadJson, + const QString& targetUserId, + const QString& targetDeviceId) const; #endif void saveAccessTokenToKeychain() const @@ -1365,17 +1364,10 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) } SendToDeviceJob* Connection::sendToDevices( - const QString& eventType, const UsersToDevicesToEvents& eventsMap) + const QString& eventType, const UsersToDevicesToContent& contents) { - QHash> json; - json.reserve(int(eventsMap.size())); - for (const auto& [userId, devicesToEvents] : eventsMap) { - auto& jsonUser = json[userId]; - for (const auto& [deviceId, event] : devicesToEvents) - jsonUser.insert(deviceId, event->contentJson()); - } return callApi(BackgroundRequest, eventType, - generateTxnId(), json); + generateTxnId(), contents); } SendMessageJob* Connection::sendMessage(const QString& roomId, @@ -2354,30 +2346,15 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, return true; } -std::unique_ptr Connection::Private::makeEventForSessionKey( - const QString& roomId, const QString& targetUserId, - const QString& targetDeviceId, const QByteArray& sessionId, - const QByteArray& sessionKey) const +QJsonObject Connection::Private::encryptSessionKeyEvent( + QJsonObject payloadJson, const QString& targetUserId, + const QString& targetDeviceId) const { - // Noisy but nice for debugging - // qDebug(E2EE) << "Creating the payload for" << data->userId() << device << - // sessionId << sessionKey.toHex(); - const auto event = makeEvent("m.megolm.v1.aes-sha2", roomId, - sessionId, sessionKey, - data->userId()); - auto payloadJson = event->fullJson(); payloadJson.insert("recipient"_ls, targetUserId); - payloadJson.insert(SenderKeyL, data->userId()); payloadJson.insert("recipient_keys"_ls, QJsonObject { { Ed25519Key, edKeyForUserDevice(targetUserId, targetDeviceId) } }); - payloadJson.insert("keys"_ls, - QJsonObject { - { Ed25519Key, - QString(olmAccount->identityKeys().ed25519) } }); - payloadJson.insert("sender_device"_ls, data->deviceId()); - const auto [type, cipherText] = olmEncryptMessage( targetUserId, targetDeviceId, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); @@ -2387,8 +2364,8 @@ std::unique_ptr Connection::Private::makeEventForSessionKey( { "body"_ls, QString(cipherText) } } } }; - return makeEvent(encrypted, - olmAccount->identityKeys().curve25519); + return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519) + .contentJson(); } void Connection::sendSessionKeyToDevices( @@ -2409,11 +2386,21 @@ void Connection::sendSessionKeyToDevices( if (hash.isEmpty()) return; + auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, roomId, sessionId, + sessionKey, userId()) + .fullJson(); + keyEventJson.insert(SenderKeyL, userId()); + keyEventJson.insert("sender_device"_ls, deviceId()); + keyEventJson.insert( + "keys"_ls, + QJsonObject { + { Ed25519Key, QString(olmAccount()->identityKeys().ed25519) } }); + auto job = callApi(hash); - connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { - UsersToDevicesToEvents usersToDevicesToEvents; - const auto oneTimeKeys = job->oneTimeKeys(); - for (const auto& [targetUserId, targetDeviceId] : + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, keyEventJson, devices, index] { + QHash> usersToDevicesToContent; + for (const auto oneTimeKeys = job->oneTimeKeys(); + const auto& [targetUserId, targetDeviceId] : asKeyValueRange(devices)) { if (!hasOlmSession(targetUserId, targetDeviceId) && !d->createOlmSession( @@ -2421,12 +2408,15 @@ void Connection::sendSessionKeyToDevices( oneTimeKeys[targetUserId][targetDeviceId])) continue; - usersToDevicesToEvents[targetUserId][targetDeviceId] = - d->makeEventForSessionKey(roomId, targetUserId, targetDeviceId, - sessionId, sessionKey); + // Noisy but nice for debugging +// qDebug(E2EE) << "Creating the payload for" << targetUserId +// << targetDeviceId << sessionId << sessionKey.toHex(); + usersToDevicesToContent[targetUserId][targetDeviceId] = + d->encryptSessionKeyEvent(keyEventJson, targetUserId, + targetDeviceId); } - if (!usersToDevicesToEvents.empty()) { - sendToDevices(EncryptedEvent::TypeId, usersToDevicesToEvents); + if (!usersToDevicesToContent.empty()) { + sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent); QVector> receivedDevices; receivedDevices.reserve(devices.size()); for (const auto& [user, device] : asKeyValueRange(devices)) diff --git a/lib/connection.h b/lib/connection.h index 5b806350..b8246ecb 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -133,8 +133,7 @@ class QUOTIENT_API Connection : public QObject { Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded) public: - using UsersToDevicesToEvents = - UnorderedMap>; + using UsersToDevicesToContent = QHash>; enum RoomVisibility { PublishRoom, @@ -689,7 +688,7 @@ public Q_SLOTS: ForgetRoomJob* forgetRoom(const QString& id); SendToDeviceJob* sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap); + const UsersToDevicesToContent& contents); /** \deprecated This method is experimental and may be removed any time */ SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event); -- 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. --- .github/workflows/ci.yml | 5 +++-- lib/accountregistry.h | 25 +++++++++++++------------ lib/avatar.cpp | 8 ++++---- lib/connection.cpp | 4 ++-- lib/room.cpp | 4 ++-- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab581238..f03af94b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: max-parallel: 1 matrix: os: [ ubuntu-20.04, macos-10.15 ] - qt-version: [ '5.15.2', '6.3.1' ] + qt-version: [ '6.3.1', '5.15.2' ] compiler: [ LLVM ] # Not using binary values here, to make the job captions more readable e2ee: [ '', e2ee ] @@ -105,7 +105,8 @@ jobs: -DBUILD_SHARED_LIBS=${{ runner.os == 'Linux' }} \ -DCMAKE_INSTALL_PREFIX=~/.local \ -DCMAKE_PREFIX_PATH=~/.local \ - -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON \ + -DBUILD_WITH_QT6=${{ startsWith(matrix.qt-version, '6') }}" if [ '${{ matrix.static-analysis }}' == 'sonar' ]; then mkdir -p $HOME/.sonar diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 38cfe6c6..9560688e 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -31,8 +31,9 @@ class QUOTIENT_API AccountRegistry : public QAbstractListModel, /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged) public: - using const_iterator = QVector::const_iterator; - using const_reference = QVector::const_reference; + using vector_t = QVector; + using const_iterator = vector_t::const_iterator; + using const_reference = vector_t::const_reference; enum EventRoles { AccountRole = Qt::UserRole + 1, @@ -42,24 +43,24 @@ public: [[deprecated("Use Accounts variable instead")]] // static AccountRegistry& instance(); - // Expose most of QVector's const-API but only provide add() and drop() + // Expose most of vector_t's const-API but only provide add() and drop() // for changing it. In theory other changing operations could be supported // too; but then boilerplate begin/end*() calls has to be tucked into each // and this class gives no guarantees on the order of entries, so why care. - const QVector& accounts() const { return *this; } + const vector_t& accounts() const { return *this; } void add(Connection* a); void drop(Connection* a); - const_iterator begin() const { return QVector::begin(); } - const_iterator end() const { return QVector::end(); } - const_reference front() const { return QVector::front(); } - const_reference back() const { return QVector::back(); } + const_iterator begin() const { return vector_t::begin(); } + const_iterator end() const { return vector_t::end(); } + const_reference front() const { return vector_t::front(); } + const_reference back() const { return vector_t::back(); } bool isLoggedIn(const QString& userId) const; Connection* get(const QString& userId); - using QVector::isEmpty, QVector::empty; - using QVector::size, QVector::count, QVector::capacity; - using QVector::cbegin, QVector::cend, QVector::contains; + using vector_t::isEmpty, vector_t::empty; + using vector_t::size, vector_t::count, vector_t::capacity; + using vector_t::cbegin, vector_t::cend, vector_t::contains; // QAbstractItemModel interface implementation @@ -88,4 +89,4 @@ private: inline QUOTIENT_API AccountRegistry Accounts {}; inline AccountRegistry& AccountRegistry::instance() { return Accounts; } -} +} // namespace Quotient diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 9304a3de..13de99bf 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -39,7 +39,7 @@ public: // The below are related to image caching, hence mutable mutable QImage _originalImage; - mutable std::vector> _scaledImages; + mutable std::vector> _scaledImages; mutable QSize _requestedSize; mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; mutable QPointer _thumbnailRequest = nullptr; @@ -124,9 +124,9 @@ QImage Avatar::Private::get(Connection* connection, QSize size, }); } - for (const auto& p : _scaledImages) - if (p.first == size) - return p.second; + for (const auto& [scaledSize, scaledImage] : _scaledImages) + if (scaledSize == size) + return scaledImage; auto result = _originalImage.isNull() ? QImage() : _originalImage.scaled(size, Qt::KeepAspectRatio, diff --git a/lib/connection.cpp b/lib/connection.cpp index 3e44513b..c390cc05 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -92,7 +92,7 @@ public: // state is Invited. The spec mandates to keep Invited room state // separately; specifically, we should keep objects for Invite and // Leave state of the same room if the two happen to co-exist. - QHash, Room*> roomMap; + QHash, Room*> roomMap; /// Mapping from serverparts to alias/room id mappings, /// as of the last sync QHash roomAliasMap; @@ -1707,7 +1707,7 @@ Room* Connection::provideRoom(const QString& id, Omittable joinState) Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); // If joinState is empty, all joinState == comparisons below are false. - const auto roomKey = qMakePair(id, joinState == JoinState::Invite); + const std::pair roomKey { id, joinState == JoinState::Invite }; auto* room = d->roomMap.value(roomKey, nullptr); if (room) { // Leave is a special case because in transition (5a) (see the .h file) 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 3581b9d03fe2f169909b3977606abd3b459c0529 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 16 Jun 2022 22:44:41 +0200 Subject: CMakeLists and elsewhere: require Qt 5.15 --- CMakeLists.txt | 2 +- CONTRIBUTING.md | 9 +-------- README.md | 16 +++++++--------- libquotient.pri | 3 +-- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index efdd5bb6..048d3b07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,7 @@ option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF) if (BUILD_WITH_QT6) set(QtMinVersion "6.0") else() - set(QtMinVersion "5.12") + set(QtMinVersion "5.15") set(QtExtraModules "Multimedia") # See #483 endif() string(REGEX REPLACE "^(.).*" "Qt\\1" Qt ${QtMinVersion}) # makes "Qt5" or "Qt6" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc65abf3..7a5ee079 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,14 +154,7 @@ just don't bankrupt us with it. Refactoring is welcome. ### Code style and formatting -As of Quotient 0.7, the C++ standard for newly written code is C++20 with a few -restrictions, notably: -* enumerators and slots cannot have `[[attributes]]` because moc from Qt 5.12 - chokes on them - this will be lifted when we move on to Qt 5.13 for the oldest - supported version, in the meantime use `Q_DECL_DEPRECATED` and similar Qt - macros - they expand to nothing when the code is passed to moc. -* explicit lists in lambda captures are preferred over `[=]`; note that C++20 - deprecates implicit `this` capture in `[=]`. +As of Quotient 0.7, the C++ standard for newly written code is C++20. The code style is defined by `.clang-format`, and in general, all C++ files should follow it. Files with minor deviations from the defined style are still diff --git a/README.md b/README.md index e42cb488..2deaa28b 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,17 @@ If you find what looks like a security issue, please use instructions in SECURITY.md. ## Getting and using libQuotient -Depending on your platform, the library can come as a separate package. -Recent releases of Debian and openSUSE, e.g., already have the package -(under the old name). If your Linux repo doesn't provide binary package -(either libqmatrixclient - older - or libquotient - newer), or you're -on Windows or macOS, your best bet is to build the library from the source -and bundle it with your application. +Depending on your platform, the library can be obtained from a package +management system. Recent releases of Debian and openSUSE, e.g., already have +it. Alternatively, just build the library from the source and bundle it with +your application, as described below. ### Pre-requisites - A recent Linux, macOS or Windows system (desktop versions are known to work; mobile operating systems where Qt is available might work too) - - Recent enough Linux examples: Debian Bullseye; Fedora 33; openSUSE Leap 15.3; - Ubuntu Focal Fossa. -- Qt 5 (either Open Source or Commercial), 5.12 or higher + - Recent enough Linux examples: Debian Bullseye; Fedora 35; + openSUSE Leap 15.4; Ubuntu 22.04 LTS. +- Qt 5 (either Open Source or Commercial), 5.15 or higher - CMake 3.16 or newer (from your package management system or [the official website](https://cmake.org/download/)) - A C++ toolchain with that supports at least some subset of C++20: diff --git a/libquotient.pri b/libquotient.pri index 677f60d3..1b4bd9c0 100644 --- a/libquotient.pri +++ b/libquotient.pri @@ -1,8 +1,7 @@ QT += network multimedia QT -= gui -# TODO: Having moved to Qt 5.12, replace c++1z with c++17 below -CONFIG *= c++1z warn_on rtti_off create_prl object_parallel_to_source +CONFIG *= c++20 warn_on rtti_off create_prl object_parallel_to_source win32-msvc* { # Quotient code base does not play well with NMake inference rules -- cgit v1.2.3 From 56575aaed81e1d55e41f577d7b6683702e4c0384 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 14 Jun 2022 11:22:06 +0200 Subject: Replace LGTM badge with GHA/Sonar Also: add a Matrix chat badge. [skip ci] --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2deaa28b..e0f4596c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,10 @@ [![release](https://img.shields.io/github/release/quotient-im/libQuotient/all.svg)](https://github.com/quotient-im/libQuotient/releases/latest) [![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) ![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg) -[![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/quotient-im/libQuotient.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quotient-im/libQuotient/context:cpp) +![CI Status](https://img.shields.io/github/workflow/status/quotient-im/libQuotient/CI) +![Sonar Tech Debt](https://img.shields.io/sonar/tech_debt/quotient-im_libQuotient?server=https%3A%2F%2Fsonarcloud.io) +![Sonar Coverage](https://img.shields.io/sonar/coverage/quotient-im_libQuotient?server=https%3A%2F%2Fsonarcloud.io) +![Matrix](https://img.shields.io/matrix/quotient:matrix.org?logo=matrix) The Quotient project aims to produce a Qt5-based SDK to develop applications for [Matrix](https://matrix.org). libQuotient is a library that enables client -- cgit v1.2.3 From 10867950474a360426685ad888237a5542b0cfac Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 16 Jun 2022 13:28:35 +0200 Subject: operation.cpp.mustache: streamline RequestData construction That `std::move(_data)` never worked because the passed object is a precursor to RequestData, and RequestData always takes things by const-ref or by value, never by rvalue. Also, explicit mention of RequestData is unnecessary, as its constructors are implicit by design. --- gtad/operation.cpp.mustache | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache index 3d26ec73..4b75434c 100644 --- a/gtad/operation.cpp.mustache +++ b/gtad/operation.cpp.mustache @@ -34,20 +34,20 @@ QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}}, { {{#headerParams}} setRequestHeader("{{baseName}}", {{paramName}}.toLatin1()); {{/headerParams}}{{#inlineBody}}{{^propertyMap}}{{^bodyParams?}} - setRequestData(RequestData({{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? - }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}})); + setRequestData({ {{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? + }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}} }); {{/bodyParams?}}{{/propertyMap}}{{/inlineBody }}{{^consumesNonJson?}}{{#bodyParams?}} - QJsonObject _data; + QJsonObject _dataJson; {{#propertyMap}} - fillJson(_data, {{nameCamelCase}}); + fillJson(_dataJson, {{nameCamelCase}}); {{/propertyMap}}{{#inlineBody}} - fillJson<{{>maybeOmittableType}}>(_data, {{paramName}}); + fillJson<{{>maybeOmittableType}}>(_dataJson, {{paramName}}); {{/inlineBody}}{{#bodyParams}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(_data, + addParam<{{^required?}}IfNotEmpty{{/required?}}>(_dataJson, QStringLiteral("{{baseName}}"), {{paramName}}); {{/bodyParams}} - setRequestData(std::move(_data)); + setRequestData({ _dataJson }); {{/bodyParams?}}{{/consumesNonJson?}}{{#producesNonJson?}} setExpectedContentTypes({ {{#produces}}"{{_}}"{{>cjoin}}{{/produces}} }); {{/producesNonJson?}}{{^producesNonJson? -- cgit v1.2.3 From 2dd85770cbfd6d9c7506757f25765c05ef74987d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 16 Jun 2022 13:29:02 +0200 Subject: Regenerate API files upon the previous commit --- lib/csapi/account-data.cpp | 4 +-- lib/csapi/administrative_contact.cpp | 52 ++++++++++++++++----------------- lib/csapi/appservice_room_directory.cpp | 6 ++-- lib/csapi/banning.cpp | 16 +++++----- lib/csapi/content-repo.cpp | 2 +- lib/csapi/create_room.cpp | 30 ++++++++++--------- lib/csapi/cross_signing.cpp | 14 ++++----- lib/csapi/device_management.cpp | 20 ++++++------- lib/csapi/directory.cpp | 6 ++-- lib/csapi/filter.cpp | 2 +- lib/csapi/inviting.cpp | 8 ++--- lib/csapi/joining.cpp | 16 +++++----- lib/csapi/keys.cpp | 30 ++++++++++--------- lib/csapi/kicking.cpp | 8 ++--- lib/csapi/knocking.cpp | 6 ++-- lib/csapi/leaving.cpp | 6 ++-- lib/csapi/list_public_rooms.cpp | 20 ++++++------- lib/csapi/login.cpp | 20 +++++++------ lib/csapi/openid.cpp | 2 +- lib/csapi/presence.cpp | 8 ++--- lib/csapi/profile.cpp | 12 ++++---- lib/csapi/pusher.cpp | 23 ++++++++------- lib/csapi/pushrules.cpp | 22 +++++++------- lib/csapi/read_markers.cpp | 8 ++--- lib/csapi/receipts.cpp | 2 +- lib/csapi/redaction.cpp | 6 ++-- lib/csapi/refresh.cpp | 7 +++-- lib/csapi/registration.cpp | 48 ++++++++++++++++-------------- lib/csapi/registration.h | 4 +-- lib/csapi/report_content.cpp | 8 ++--- lib/csapi/room_send.cpp | 2 +- lib/csapi/room_state.cpp | 2 +- lib/csapi/room_upgrades.cpp | 6 ++-- lib/csapi/search.cpp | 6 ++-- lib/csapi/tags.cpp | 8 ++--- lib/csapi/third_party_membership.cpp | 12 ++++---- lib/csapi/to_device.cpp | 6 ++-- lib/csapi/typing.cpp | 8 ++--- lib/csapi/users.cpp | 8 ++--- 39 files changed, 243 insertions(+), 231 deletions(-) diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 1343eb98..8c71f6c5 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -12,7 +12,7 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)) { - setRequestData(RequestData(toJson(content))); + setRequestData({ toJson(content) }); } QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, @@ -37,7 +37,7 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)) { - setRequestData(RequestData(toJson(content))); + setRequestData({ toJson(content) }); } QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index f52e2e1f..aa55d934 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -21,9 +21,9 @@ Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), makePath("/_matrix/client/v3", "/account/3pid")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("three_pid_creds"), threePidCreds); + setRequestData({ _dataJson }); } Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, @@ -31,11 +31,11 @@ Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), makePath("/_matrix/client/v3", "/account/3pid/add")) { - QJsonObject _data; - addParam(_data, QStringLiteral("auth"), auth); - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("sid"), sid); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("auth"), auth); + addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret); + addParam<>(_dataJson, QStringLiteral("sid"), sid); + setRequestData({ _dataJson }); } Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer, @@ -43,12 +43,12 @@ Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer, : BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"), makePath("/_matrix/client/v3", "/account/3pid/bind")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken); - addParam<>(_data, QStringLiteral("sid"), sid); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret); + addParam<>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_dataJson, QStringLiteral("sid"), sid); + setRequestData({ _dataJson }); } Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, @@ -57,11 +57,11 @@ Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, : BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"), makePath("/_matrix/client/v3", "/account/3pid/delete")) { - QJsonObject _data; - addParam(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); addExpectedKey("id_server_unbind_result"); } @@ -71,11 +71,11 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), makePath("/_matrix/client/v3", "/account/3pid/unbind")) { - QJsonObject _data; - addParam(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); addExpectedKey("id_server_unbind_result"); } @@ -86,7 +86,7 @@ RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( "/account/3pid/email/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( @@ -96,5 +96,5 @@ RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( "/account/3pid/msisdn/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index c989559f..dff7e032 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -15,7 +15,7 @@ UpdateAppserviceRoomDirectoryVisibilityJob:: makePath("/_matrix/client/v3", "/directory/list/appservice/", networkId, "/", roomId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("visibility"), visibility); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("visibility"), visibility); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 77047e89..e04075b7 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -11,10 +11,10 @@ BanJob::BanJob(const QString& roomId, const QString& userId, : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/ban")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } UnbanJob::UnbanJob(const QString& roomId, const QString& userId, @@ -22,8 +22,8 @@ UnbanJob::UnbanJob(const QString& roomId, const QString& userId, : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/unban")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 7d740cb7..6f6738af 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -20,7 +20,7 @@ UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - setRequestData(RequestData(content)); + setRequestData({ content }); addExpectedKey("content_uri"); } diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 834d8c13..afae80af 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -18,22 +18,24 @@ CreateRoomJob::CreateRoomJob(const QString& visibility, : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), makePath("/_matrix/client/v3", "/createRoom")) { - QJsonObject _data; - addParam(_data, QStringLiteral("visibility"), visibility); - addParam(_data, QStringLiteral("room_alias_name"), + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("visibility"), visibility); + addParam(_dataJson, QStringLiteral("room_alias_name"), roomAliasName); - addParam(_data, QStringLiteral("name"), name); - addParam(_data, QStringLiteral("topic"), topic); - addParam(_data, QStringLiteral("invite"), invite); - addParam(_data, QStringLiteral("invite_3pid"), invite3pid); - addParam(_data, QStringLiteral("room_version"), roomVersion); - addParam(_data, QStringLiteral("creation_content"), + addParam(_dataJson, QStringLiteral("name"), name); + addParam(_dataJson, QStringLiteral("topic"), topic); + addParam(_dataJson, QStringLiteral("invite"), invite); + addParam(_dataJson, QStringLiteral("invite_3pid"), invite3pid); + addParam(_dataJson, QStringLiteral("room_version"), roomVersion); + addParam(_dataJson, QStringLiteral("creation_content"), creationContent); - addParam(_data, QStringLiteral("initial_state"), initialState); - addParam(_data, QStringLiteral("preset"), preset); - addParam(_data, QStringLiteral("is_direct"), isDirect); - addParam(_data, QStringLiteral("power_level_content_override"), + addParam(_dataJson, QStringLiteral("initial_state"), + initialState); + addParam(_dataJson, QStringLiteral("preset"), preset); + addParam(_dataJson, QStringLiteral("is_direct"), isDirect); + addParam(_dataJson, + QStringLiteral("power_level_content_override"), powerLevelContentOverride); - setRequestData(std::move(_data)); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp index c6c34772..83136d71 100644 --- a/lib/csapi/cross_signing.cpp +++ b/lib/csapi/cross_signing.cpp @@ -14,14 +14,14 @@ UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), makePath("/_matrix/client/v3", "/keys/device_signing/upload")) { - QJsonObject _data; - addParam(_data, QStringLiteral("master_key"), masterKey); - addParam(_data, QStringLiteral("self_signing_key"), + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("master_key"), masterKey); + addParam(_dataJson, QStringLiteral("self_signing_key"), selfSigningKey); - addParam(_data, QStringLiteral("user_signing_key"), + addParam(_dataJson, QStringLiteral("user_signing_key"), userSigningKey); - addParam(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + addParam(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( @@ -29,5 +29,5 @@ UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), makePath("/_matrix/client/v3", "/keys/signatures/upload")) { - setRequestData(RequestData(toJson(signatures))); + setRequestData({ toJson(signatures) }); } diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index fb58633c..6f2badee 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -34,9 +34,9 @@ UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), makePath("/_matrix/client/v3", "/devices/", deviceId)) { - QJsonObject _data; - addParam(_data, QStringLiteral("display_name"), displayName); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("display_name"), displayName); + setRequestData({ _dataJson }); } DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, @@ -44,9 +44,9 @@ DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), makePath("/_matrix/client/v3", "/devices/", deviceId)) { - QJsonObject _data; - addParam(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, @@ -54,8 +54,8 @@ DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), makePath("/_matrix/client/v3", "/delete_devices")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("devices"), devices); - addParam(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("devices"), devices); + addParam(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 86b14f3a..c1255bb1 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -10,9 +10,9 @@ SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), makePath("/_matrix/client/v3", "/directory/room/", roomAlias)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("room_id"), roomId); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("room_id"), roomId); + setRequestData({ _dataJson }); } QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 57cb1271..2469fbd1 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -10,7 +10,7 @@ DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), makePath("/_matrix/client/v3", "/user/", userId, "/filter")) { - setRequestData(RequestData(toJson(filter))); + setRequestData({ toJson(filter) }); addExpectedKey("filter_id"); } diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index bc1863ce..41a8b5be 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -11,8 +11,8 @@ InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index b05bd964..cdba95e9 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -12,11 +12,11 @@ JoinRoomByIdJob::JoinRoomByIdJob( : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/join")) { - QJsonObject _data; - addParam(_data, QStringLiteral("third_party_signed"), + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("third_party_signed"), thirdPartySigned); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } @@ -35,10 +35,10 @@ JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, makePath("/_matrix/client/v3", "/join/", roomIdOrAlias), queryToJoinRoom(serverName)) { - QJsonObject _data; - addParam(_data, QStringLiteral("third_party_signed"), + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("third_party_signed"), thirdPartySigned); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index d4996664..2e4978f2 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -12,11 +12,13 @@ UploadKeysJob::UploadKeysJob(const Omittable& deviceKeys, : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), makePath("/_matrix/client/v3", "/keys/upload")) { - QJsonObject _data; - addParam(_data, QStringLiteral("device_keys"), deviceKeys); - addParam(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - addParam(_data, QStringLiteral("fallback_keys"), fallbackKeys); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("device_keys"), deviceKeys); + addParam(_dataJson, QStringLiteral("one_time_keys"), + oneTimeKeys); + addParam(_dataJson, QStringLiteral("fallback_keys"), + fallbackKeys); + setRequestData({ _dataJson }); addExpectedKey("one_time_key_counts"); } @@ -25,11 +27,11 @@ QueryKeysJob::QueryKeysJob(const QHash& deviceKeys, : BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"), makePath("/_matrix/client/v3", "/keys/query")) { - QJsonObject _data; - addParam(_data, QStringLiteral("timeout"), timeout); - addParam<>(_data, QStringLiteral("device_keys"), deviceKeys); - addParam(_data, QStringLiteral("token"), token); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("timeout"), timeout); + addParam<>(_dataJson, QStringLiteral("device_keys"), deviceKeys); + addParam(_dataJson, QStringLiteral("token"), token); + setRequestData({ _dataJson }); } ClaimKeysJob::ClaimKeysJob( @@ -38,10 +40,10 @@ ClaimKeysJob::ClaimKeysJob( : BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"), makePath("/_matrix/client/v3", "/keys/claim")) { - QJsonObject _data; - addParam(_data, QStringLiteral("timeout"), timeout); - addParam<>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("timeout"), timeout); + addParam<>(_dataJson, QStringLiteral("one_time_keys"), oneTimeKeys); + setRequestData({ _dataJson }); addExpectedKey("one_time_keys"); } diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 3bedcb34..4ca39c4c 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -11,8 +11,8 @@ KickJob::KickJob(const QString& roomId, const QString& userId, : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/kick")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp index ba541643..b9da4b9b 100644 --- a/lib/csapi/knocking.cpp +++ b/lib/csapi/knocking.cpp @@ -19,8 +19,8 @@ KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias, makePath("/_matrix/client/v3", "/knock/", roomIdOrAlias), queryToKnockRoom(serverName)) { - QJsonObject _data; - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index 84340b94..ba91f26a 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -10,9 +10,9 @@ LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/leave")) { - QJsonObject _data; - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 417e50b3..4deecfc2 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -26,9 +26,9 @@ SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"), makePath("/_matrix/client/v3", "/directory/list/room/", roomId)) { - QJsonObject _data; - addParam(_data, QStringLiteral("visibility"), visibility); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("visibility"), visibility); + setRequestData({ _dataJson }); } auto queryToGetPublicRooms(Omittable limit, const QString& since, @@ -77,14 +77,14 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, makePath("/_matrix/client/v3", "/publicRooms"), queryToQueryPublicRooms(server)) { - QJsonObject _data; - addParam(_data, QStringLiteral("limit"), limit); - addParam(_data, QStringLiteral("since"), since); - addParam(_data, QStringLiteral("filter"), filter); - addParam(_data, QStringLiteral("include_all_networks"), + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("limit"), limit); + addParam(_dataJson, QStringLiteral("since"), since); + addParam(_dataJson, QStringLiteral("filter"), filter); + addParam(_dataJson, QStringLiteral("include_all_networks"), includeAllNetworks); - addParam(_data, QStringLiteral("third_party_instance_id"), + addParam(_dataJson, QStringLiteral("third_party_instance_id"), thirdPartyInstanceId); - setRequestData(std::move(_data)); + setRequestData({ _dataJson }); addExpectedKey("chunk"); } diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 5e007d8f..81e603b5 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -26,14 +26,16 @@ LoginJob::LoginJob(const QString& type, : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), makePath("/_matrix/client/v3", "/login"), false) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("type"), type); - addParam(_data, QStringLiteral("identifier"), identifier); - addParam(_data, QStringLiteral("password"), password); - addParam(_data, QStringLiteral("token"), token); - addParam(_data, QStringLiteral("device_id"), deviceId); - addParam(_data, QStringLiteral("initial_device_display_name"), + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("type"), type); + addParam(_dataJson, QStringLiteral("identifier"), identifier); + addParam(_dataJson, QStringLiteral("password"), password); + addParam(_dataJson, QStringLiteral("token"), token); + addParam(_dataJson, QStringLiteral("device_id"), deviceId); + addParam(_dataJson, + QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - addParam(_data, QStringLiteral("refresh_token"), refreshToken); - setRequestData(std::move(_data)); + addParam(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 8349e6db..7e89b8a6 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -12,5 +12,5 @@ RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, makePath("/_matrix/client/v3", "/user/", userId, "/openid/request_token")) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 6d154ebd..828ccfb7 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -11,10 +11,10 @@ SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("presence"), presence); - addParam(_data, QStringLiteral("status_msg"), statusMsg); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("presence"), presence); + addParam(_dataJson, QStringLiteral("status_msg"), statusMsg); + setRequestData({ _dataJson }); } QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 7621d828..f024ed82 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -12,9 +12,9 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId, makePath("/_matrix/client/v3", "/profile/", userId, "/displayname")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("displayname"), displayname); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("displayname"), displayname); + setRequestData({ _dataJson }); } QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) @@ -35,9 +35,9 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("avatar_url"), avatarUrl); + setRequestData({ _dataJson }); } QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index 498be3ee..fb6595fc 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -25,15 +25,16 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, : BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"), makePath("/_matrix/client/v3", "/pushers/set")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("pushkey"), pushkey); - addParam<>(_data, QStringLiteral("kind"), kind); - addParam<>(_data, QStringLiteral("app_id"), appId); - addParam<>(_data, QStringLiteral("app_display_name"), appDisplayName); - addParam<>(_data, QStringLiteral("device_display_name"), deviceDisplayName); - addParam(_data, QStringLiteral("profile_tag"), profileTag); - addParam<>(_data, QStringLiteral("lang"), lang); - addParam<>(_data, QStringLiteral("data"), data); - addParam(_data, QStringLiteral("append"), append); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("pushkey"), pushkey); + addParam<>(_dataJson, QStringLiteral("kind"), kind); + addParam<>(_dataJson, QStringLiteral("app_id"), appId); + addParam<>(_dataJson, QStringLiteral("app_display_name"), appDisplayName); + addParam<>(_dataJson, QStringLiteral("device_display_name"), + deviceDisplayName); + addParam(_dataJson, QStringLiteral("profile_tag"), profileTag); + addParam<>(_dataJson, QStringLiteral("lang"), lang); + addParam<>(_dataJson, QStringLiteral("data"), data); + addParam(_dataJson, QStringLiteral("append"), append); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index 6b0effd4..2376654a 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -69,11 +69,11 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, "/", ruleId), queryToSetPushRule(before, after)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("actions"), actions); - addParam(_data, QStringLiteral("conditions"), conditions); - addParam(_data, QStringLiteral("pattern"), pattern); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("actions"), actions); + addParam(_dataJson, QStringLiteral("conditions"), conditions); + addParam(_dataJson, QStringLiteral("pattern"), pattern); + setRequestData({ _dataJson }); } QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, @@ -103,9 +103,9 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/enabled")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("enabled"), enabled); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("enabled"), enabled); + setRequestData({ _dataJson }); } QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, @@ -136,7 +136,7 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/actions")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("actions"), actions); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("actions"), actions); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index dc84f887..de5f4a9a 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -12,8 +12,8 @@ SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/read_markers")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); - addParam(_data, QStringLiteral("m.read"), mRead); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead); + addParam(_dataJson, QStringLiteral("m.read"), mRead); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 8feab986..0194603d 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -13,5 +13,5 @@ PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType makePath("/_matrix/client/v3", "/rooms/", roomId, "/receipt/", receiptType, "/", eventId)) { - setRequestData(RequestData(toJson(receipt))); + setRequestData({ toJson(receipt) }); } diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index d67cb37b..154abd9b 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -12,7 +12,7 @@ RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, makePath("/_matrix/client/v3", "/rooms/", roomId, "/redact/", eventId, "/", txnId)) { - QJsonObject _data; - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/refresh.cpp b/lib/csapi/refresh.cpp index 8d4a34ae..284ae4ff 100644 --- a/lib/csapi/refresh.cpp +++ b/lib/csapi/refresh.cpp @@ -10,8 +10,9 @@ RefreshJob::RefreshJob(const QString& refreshToken) : BaseJob(HttpVerb::Post, QStringLiteral("RefreshJob"), makePath("/_matrix/client/v3", "/refresh"), false) { - QJsonObject _data; - addParam(_data, QStringLiteral("refresh_token"), refreshToken); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); addExpectedKey("access_token"); } diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 3541724b..04c0fe12 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -24,16 +24,19 @@ RegisterJob::RegisterJob(const QString& kind, makePath("/_matrix/client/v3", "/register"), queryToRegister(kind), {}, false) { - QJsonObject _data; - addParam(_data, QStringLiteral("auth"), auth); - addParam(_data, QStringLiteral("username"), username); - addParam(_data, QStringLiteral("password"), password); - addParam(_data, QStringLiteral("device_id"), deviceId); - addParam(_data, QStringLiteral("initial_device_display_name"), + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("auth"), auth); + addParam(_dataJson, QStringLiteral("username"), username); + addParam(_dataJson, QStringLiteral("password"), password); + addParam(_dataJson, QStringLiteral("device_id"), deviceId); + addParam(_dataJson, + QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - addParam(_data, QStringLiteral("inhibit_login"), inhibitLogin); - addParam(_data, QStringLiteral("refresh_token"), refreshToken); - setRequestData(std::move(_data)); + addParam(_dataJson, QStringLiteral("inhibit_login"), + inhibitLogin); + addParam(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); addExpectedKey("user_id"); } @@ -43,7 +46,7 @@ RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( makePath("/_matrix/client/v3", "/register/email/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( @@ -52,7 +55,7 @@ RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( makePath("/_matrix/client/v3", "/register/msisdn/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } ChangePasswordJob::ChangePasswordJob(const QString& newPassword, @@ -61,11 +64,12 @@ ChangePasswordJob::ChangePasswordJob(const QString& newPassword, : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), makePath("/_matrix/client/v3", "/account/password")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("new_password"), newPassword); - addParam(_data, QStringLiteral("logout_devices"), logoutDevices); - addParam(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("new_password"), newPassword); + addParam(_dataJson, QStringLiteral("logout_devices"), + logoutDevices); + addParam(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( @@ -76,7 +80,7 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( "/account/password/email/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( @@ -87,7 +91,7 @@ RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( "/account/password/msisdn/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } DeactivateAccountJob::DeactivateAccountJob( @@ -95,10 +99,10 @@ DeactivateAccountJob::DeactivateAccountJob( : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), makePath("/_matrix/client/v3", "/account/deactivate")) { - QJsonObject _data; - addParam(_data, QStringLiteral("auth"), auth); - addParam(_data, QStringLiteral("id_server"), idServer); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("auth"), auth); + addParam(_dataJson, QStringLiteral("id_server"), idServer); + setRequestData({ _dataJson }); addExpectedKey("id_server_unbind_result"); } diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 7a20cab8..21d7f9d7 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -126,7 +126,7 @@ public: /// obtain a new access token when it expires by calling the /// `/refresh` endpoint. /// - /// Omitted if the `inhibit_login` option is false. + /// Omitted if the `inhibit_login` option is true. QString refreshToken() const { return loadFromJson("refresh_token"_ls); @@ -139,7 +139,7 @@ public: /// to obtain a new access token. If not given, the client can /// assume that the access token will not expire. /// - /// Omitted if the `inhibit_login` option is false. + /// Omitted if the `inhibit_login` option is true. Omittable expiresInMs() const { return loadFromJson>("expires_in_ms"_ls); diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index b8e9a8d1..bc52208f 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -12,8 +12,8 @@ ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId makePath("/_matrix/client/v3", "/rooms/", roomId, "/report/", eventId)) { - QJsonObject _data; - addParam(_data, QStringLiteral("score"), score); - addParam(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam(_dataJson, QStringLiteral("score"), score); + addParam(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 93ab04d2..2319496f 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -12,6 +12,6 @@ SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, makePath("/_matrix/client/v3", "/rooms/", roomId, "/send/", eventType, "/", txnId)) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 2253863a..b4adb739 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -14,6 +14,6 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index 3f67234d..b03fb6e8 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -10,8 +10,8 @@ UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/upgrade")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("new_version"), newVersion); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("new_version"), newVersion); + setRequestData({ _dataJson }); addExpectedKey("replacement_room"); } diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 92300351..4e2c9e92 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -19,8 +19,8 @@ SearchJob::SearchJob(const Categories& searchCategories, makePath("/_matrix/client/v3", "/search"), queryToSearch(nextBatch)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_categories"), searchCategories); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_categories"), searchCategories); + setRequestData({ _dataJson }); addExpectedKey("search_categories"); } diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index 21cb18ed..2c85842d 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -27,10 +27,10 @@ SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags/", tag)) { - QJsonObject _data; - fillJson(_data, additionalProperties); - addParam(_data, QStringLiteral("order"), order); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + fillJson(_dataJson, additionalProperties); + addParam(_dataJson, QStringLiteral("order"), order); + setRequestData({ _dataJson }); } QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index 2d6df77d..3ca986c7 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -12,10 +12,10 @@ InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, : BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 48e943db..e10fac69 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -13,7 +13,7 @@ SendToDeviceJob::SendToDeviceJob( makePath("/_matrix/client/v3", "/sendToDevice/", eventType, "/", txnId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("messages"), messages); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("messages"), messages); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index 1b4fe147..21bd45ae 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -12,8 +12,8 @@ SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, makePath("/_matrix/client/v3", "/rooms/", roomId, "/typing/", userId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("typing"), typing); - addParam(_data, QStringLiteral("timeout"), timeout); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("typing"), typing); + addParam(_dataJson, QStringLiteral("timeout"), timeout); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index e0db6f70..c65280ee 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -11,10 +11,10 @@ SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), makePath("/_matrix/client/v3", "/user_directory/search")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_term"), searchTerm); - addParam(_data, QStringLiteral("limit"), limit); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_term"), searchTerm); + addParam(_dataJson, QStringLiteral("limit"), limit); + setRequestData({ _dataJson }); addExpectedKey("results"); addExpectedKey("limited"); } -- 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/connection.cpp | 16 ---------------- lib/converters.h | 10 +--------- lib/eventitem.h | 2 +- lib/jobs/basejob.cpp | 8 +------- lib/logging.h | 7 +------ lib/mxcreply.cpp | 8 +------- lib/networkaccessmanager.cpp | 15 +-------------- lib/quotient_common.h | 15 +-------------- lib/room.cpp | 7 ++----- lib/syncdata.cpp | 7 +------ 10 files changed, 10 insertions(+), 85 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index c390cc05..2319a38a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -49,10 +49,6 @@ # include #endif -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -# include -#endif - #include #include #include @@ -1826,16 +1822,10 @@ void Connection::saveRoomState(Room* r) const QFile outRoomFile { stateCacheDir().filePath( SyncData::fileNameForRoom(r->id())) }; if (outRoomFile.open(QFile::WriteOnly)) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(r->toJson()).toCbor() : QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { r->toJson() }; - const auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif outRoomFile.write(data.data(), data.size()); qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); } else { @@ -1905,15 +1895,9 @@ void Connection::saveState() const } #endif -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { rootObj }; - const auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; outFile.write(data.data(), data.size()); diff --git a/lib/converters.h b/lib/converters.h index da30cae6..c985fd60 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -188,15 +188,7 @@ inline QDateTime fromJson(const QJsonValue& jv) return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); } -inline QJsonValue toJson(const QDate& val) { - return toJson( -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QDateTime(val) -#else - val.startOfDay() -#endif - ); -} +inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } template <> inline QDate fromJson(const QJsonValue& jv) { diff --git a/lib/eventitem.h b/lib/eventitem.h index 5e001d88..e476c66c 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -14,7 +14,7 @@ namespace Quotient { namespace EventStatus { - QUO_NAMESPACE + Q_NAMESPACE_EXPORT(QUOTIENT_API) /** Special marks an event can assume * diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b6858b5a..fe70911e 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -301,16 +301,10 @@ void BaseJob::Private::sendRequest() QNetworkRequest::NoLessSafeRedirectPolicy); req.setMaximumRedirectsAllowed(10); req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); - req.setAttribute( -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - QNetworkRequest::Http2AllowedAttribute -#else - QNetworkRequest::HTTP2AllowedAttribute -#endif // Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at // what seems like an attempt to write to a closed channel. If/when that // changes, false should be turned to true below. - , false); + req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); Q_ASSERT(req.url().isValid()); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); diff --git a/lib/logging.h b/lib/logging.h index fc0a4c99..c8d17210 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -44,12 +44,7 @@ inline QDebug formatJson(QDebug debug_object) //! Suppress full qualification of enums/QFlags when logging inline QDebug terse(QDebug dbg) { - return -#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) - dbg.setVerbosity(0), dbg; -#else - dbg.verbosity(QDebug::MinimumVerbosity); -#endif + return dbg.verbosity(QDebug::MinimumVerbosity); } inline qint64 profilerMinNsecs() diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index 4174cfd8..c7547be8 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -71,12 +71,6 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) #endif } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -#define ERROR_SIGNAL errorOccurred -#else -#define ERROR_SIGNAL error -#endif - MxcReply::MxcReply() : d(ZeroImpl()) { @@ -88,7 +82,7 @@ MxcReply::MxcReply() setError(QNetworkReply::ProtocolInvalidOperationError, BadRequestPhrase); setFinished(true); - emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError); + emit errorOccurred(QNetworkReply::ProtocolInvalidOperationError); emit finished(); }, Qt::QueuedConnection); } diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index f4e7b1af..38ab07cc 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -68,24 +68,11 @@ void NetworkAccessManager::clearIgnoredSslErrors() d->ignoredSslErrors.clear(); } -static NetworkAccessManager* createNam() -{ - auto nam = new NetworkAccessManager(); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - // See #109; in newer Qt, bearer management is deprecated altogether - NetworkAccessManager::connect(nam, - &QNetworkAccessManager::networkAccessibleChanged, [nam] { - nam->setNetworkAccessible(QNetworkAccessManager::Accessible); - }); -#endif - return nam; -} - NetworkAccessManager* NetworkAccessManager::instance() { static QThreadStorage storage; if(!storage.hasLocalData()) { - storage.setLocalData(createNam()); + storage.setLocalData(new NetworkAccessManager()); } return storage.localData(); } diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 2b785a39..8bcd5ca6 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -51,21 +51,8 @@ #define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -// The first line forward-declares the namespace static metaobject with -// QUOTIENT_API so that dynamically linked clients could serialise flag/enum -// values from the namespace; Qt before 5.14 doesn't help with that. The second -// line is needed for moc to do its job on the namespace. -#define QUO_NAMESPACE \ - extern QUOTIENT_API const QMetaObject staticMetaObject; \ - Q_NAMESPACE -#else -// Since Qt 5.14.0, it's all packed in a single macro -#define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API) -#endif - namespace Quotient { -QUO_NAMESPACE +Q_NAMESPACE_EXPORT(QUOTIENT_API) // std::array {} needs explicit template parameters on macOS because // Apple stdlib doesn't have deduction guides for std::array. C++20 has 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); diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 95d3c7e4..ff3dc0c2 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -179,12 +179,7 @@ QJsonObject SyncData::loadJson(const QString& fileName) 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 - ; + : QCborValue::fromCbor(data).toJsonValue().toObject(); if (json.isEmpty()) { qCWarning(MAIN) << "State cache in" << fileName << "is broken or empty, discarding"; -- cgit v1.2.3 From 5a63f8a18645d612decdcc853335df0682c41d03 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 21:29:27 +0200 Subject: Drop make_array(); use std::to_array() where needed make_array() has been introduced to cover for shortcomings on macOS and Windows. These shortcomings are no more there, so we can just use the standardrlibrary. --- lib/e2ee/e2ee.h | 5 +++-- lib/events/encryptionevent.cpp | 4 +--- lib/jobs/basejob.cpp | 20 ++++++++++---------- lib/quotient_common.h | 27 +++++++-------------------- 4 files changed, 21 insertions(+), 35 deletions(-) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index f97eb27a..1efd0f16 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -37,8 +37,9 @@ constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; inline bool isSupportedAlgorithm(const QString& algorithm) { - static constexpr auto SupportedAlgorithms = - make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); + static constexpr std::array SupportedAlgorithms { + OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey + }; return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), algorithm) != SupportedAlgorithms.cend(); diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 6e994cd4..eb15f38e 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -9,9 +9,7 @@ #include namespace Quotient { -static const std::array encryptionStrings = { - { MegolmV1AesSha2AlgoKey } -}; +static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey }; template <> struct JsonConverter { diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index fe70911e..da645a2d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -138,9 +138,8 @@ public: QTimer timer; QTimer retryTimer; - static constexpr std::array errorStrategy { - { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } } - }; + static constexpr auto errorStrategy = std::to_array( + { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }); int maxRetries = int(errorStrategy.size()); int retriesTaken = 0; @@ -152,10 +151,8 @@ public: [[nodiscard]] QString dumpRequest() const { - // FIXME: use std::array {} when Apple stdlib gets deduction guides for it - static const auto verbs = - make_array(QStringLiteral("GET"), QStringLiteral("PUT"), - QStringLiteral("POST"), QStringLiteral("DELETE")); + static const std::array verbs { "GET"_ls, "PUT"_ls, "POST"_ls, + "DELETE"_ls }; const auto verbWord = verbs.at(size_t(verb)); return verbWord % ' ' % (reply ? reply->url().toString(QUrl::RemoveQuery) @@ -748,11 +745,14 @@ QString BaseJob::statusCaption() const } } -int BaseJob::error() const { return d->status.code; } +int BaseJob::error() const { + return d->status.code; } -QString BaseJob::errorString() const { return d->status.message; } +QString BaseJob::errorString() const { + return d->status.message; } -QUrl BaseJob::errorUrl() const { return d->errorUrl; } +QUrl BaseJob::errorUrl() const { + return d->errorUrl; } void BaseJob::setStatus(Status s) { diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 8bcd5ca6..136e9f79 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -41,7 +41,6 @@ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) -// Apple Clang hasn't caught up with explicit(bool) yet #if __cpp_conditional_explicit >= 201806L #define QUO_IMPLICIT explicit(false) #else @@ -54,19 +53,6 @@ namespace Quotient { Q_NAMESPACE_EXPORT(QUOTIENT_API) -// std::array {} needs explicit template parameters on macOS because -// Apple stdlib doesn't have deduction guides for std::array. C++20 has -// to_array() but that can't be borrowed, this time because of MSVC: -// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038 -// Therefore a simpler (but also slightly more wobbly - it resolves the element -// type using std::common_type<>) make_array facility is implemented here. -template -constexpr auto make_array(Ts&&... items) -{ - return std::array, sizeof...(items)>( - { std::forward(items)... }); -} - // TODO: code like this should be generated from the CS API definition //! \brief Membership states @@ -87,9 +73,10 @@ enum class Membership : unsigned int { }; QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) -constexpr auto MembershipStrings = make_array( - // The order MUST be the same as the order in the original enum - "join", "leave", "invite", "knock", "ban"); +constexpr std::array MembershipStrings { + // The order MUST be the same as the order in the Membership enum + "join", "leave", "invite", "knock", "ban" +}; //! \brief Local user join-state names //! @@ -105,10 +92,10 @@ enum class JoinState : std::underlying_type_t { }; QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) -[[maybe_unused]] constexpr auto JoinStateStrings = make_array( +[[maybe_unused]] constexpr std::array JoinStateStrings { MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ -); +}; //! \brief Network job running policy flags //! @@ -135,7 +122,7 @@ enum RoomType { }; Q_ENUM_NS(RoomType) -[[maybe_unused]] constexpr auto RoomTypeStrings = make_array("m.space"); +[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" }; } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) -- cgit v1.2.3 From 7ef84728ab3744192583eb587a4585c576f5a176 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 21:39:42 +0200 Subject: Move C++-only macros to util.h This pertains to QUO_IMPLICIT and DECL_DEPRECATED_ENUMERATOR - both can be used with no connection to Qt meta-type system (which is what quotient_common.h is for). --- lib/e2ee/e2ee.h | 3 +-- lib/events/encryptionevent.cpp | 2 -- lib/events/encryptionevent.h | 1 - lib/quotient_common.h | 9 --------- lib/util.h | 9 +++++++++ 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 1efd0f16..9501b263 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -8,7 +8,6 @@ #include "converters.h" #include "expected.h" #include "qolmerrors.h" -#include "quotient_common.h" #include #include @@ -71,7 +70,7 @@ struct IdentityKeys }; //! Struct representing the one-time keys. -struct QUOTIENT_API UnsignedOneTimeKeys +struct UnsignedOneTimeKeys { QHash> keys; diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index eb15f38e..1654d6f3 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -6,8 +6,6 @@ #include "e2ee/e2ee.h" -#include - namespace Quotient { static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey }; diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 5b5420ec..c73e5598 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -5,7 +5,6 @@ #pragma once #include "stateevent.h" -#include "quotient_common.h" namespace Quotient { class QUOTIENT_API EncryptionEventContent { diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 136e9f79..e087e7d3 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -41,15 +41,6 @@ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) -#if __cpp_conditional_explicit >= 201806L -#define QUO_IMPLICIT explicit(false) -#else -#define QUO_IMPLICIT -#endif - -#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ - Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended - namespace Quotient { Q_NAMESPACE_EXPORT(QUOTIENT_API) diff --git a/lib/util.h b/lib/util.h index 5dd69d74..d1623881 100644 --- a/lib/util.h +++ b/lib/util.h @@ -37,6 +37,15 @@ static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all QT_WARNING_POP #endif +#if __cpp_conditional_explicit >= 201806L +#define QUO_IMPLICIT explicit(false) +#else +#define QUO_IMPLICIT +#endif + +#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ + Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended + /// \brief Copy an object with slicing /// /// Unintended slicing is bad, which why there's a C++ Core Guideline that -- cgit v1.2.3 From 42f2d07fcb9f31b4e280a025472a6e810f36fb23 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 22:51:01 +0200 Subject: CI: switch to macos-11 image --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f03af94b..c71ce6c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false max-parallel: 1 matrix: - os: [ ubuntu-20.04, macos-10.15 ] + os: [ ubuntu-20.04, macos-11 ] qt-version: [ '6.3.1', '5.15.2' ] compiler: [ LLVM ] # Not using binary values here, to make the job captions more readable -- cgit v1.2.3 From 19e0251f02d547028583c2ebe9207885ff087dc4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 19 Jun 2022 23:13:14 +0200 Subject: CI: fix macos-10.15 leftover --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c71ce6c3..d619385f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - os: ubuntu-20.04 e2ee: e2ee # Will be re-added with static analysis below # TODO: Enable E2EE on Windows and macOS - - os: macos-10.15 + - os: macos-11 e2ee: e2ee include: - os: windows-2019 -- cgit v1.2.3 From 7c1125cdd146227320aa7eb082225c4051ea0563 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 19 Jun 2022 20:55:00 +0200 Subject: Add a missing #include --- lib/e2ee/e2ee.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 9501b263..234d4bcb 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -11,6 +11,7 @@ #include #include +#include namespace Quotient { -- 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(-) 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 34ff85b715b377c4b2a8e30b1af8327aa7928e36 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 18 Jun 2022 10:35:48 +0200 Subject: Move out Overloads to util.h ...instead of tucking the template in filesourceinfo.cpp where it surely will be forgotten. --- lib/events/filesourceinfo.cpp | 9 +-------- lib/util.h | 13 +++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index 11f93d80..e8b6794b 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -5,6 +5,7 @@ #include "filesourceinfo.h" #include "logging.h" +#include "util.h" #ifdef Quotient_E2EE_ENABLED # include "e2ee/qolmutils.h" @@ -140,14 +141,6 @@ void JsonObjectConverter::fillFrom(const QJsonObject& jo, JWK& pod) fromJson(jo.value("ext"_ls), pod.ext); } -template -struct Overloads : FunctorTs... { - using FunctorTs::operator()...; -}; - -template -Overloads(FunctorTs&&...) -> Overloads; - QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi) { return std::visit(Overloads { [](const QUrl& url) { return url; }, diff --git a/lib/util.h b/lib/util.h index d1623881..8a30f457 100644 --- a/lib/util.h +++ b/lib/util.h @@ -170,6 +170,19 @@ constexpr ImplPtr ZeroImpl() return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; } +//! \brief Multiplex several functors in one +//! +//! This is a well-known trick to wrap several lambdas into a single functor +//! class that can be passed to std::visit. +//! \sa https://en.cppreference.com/w/cpp/utility/variant/visit +template +struct Overloads : FunctorTs... { + using FunctorTs::operator()...; +}; + +template +Overloads(FunctorTs&&...) -> Overloads; + /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); -- cgit v1.2.3 From 3e4aa28596e0020c0b1c49f077e53339762dced3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 20 Jun 2022 18:22:12 +0200 Subject: .clang-tidy: drop some dubious noisy warnings --- .clang-tidy | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 07cef37f..488f3cc8 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-macro-usage,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' +Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' WarningsAsErrors: '' HeaderFilterRegex: '' AnalyzeTemporaryDtors: false @@ -49,10 +49,6 @@ CheckOptions: # value: 'time_t,std::time_t' # - key: cert-msc51-cpp.DisallowedSeedTypes # value: 'time_t,std::time_t' - - key: cppcoreguidelines-macro-usage.AllowedRegexp - value: '' - - key: cppcoreguidelines-macro-usage.CheckCapsOnly - value: 'false' - key: cppcoreguidelines-narrowing-conversions.IgnoreConversionFromTypes value: 'size_t;ptrdiff_t;size_type;difference_type' # - key: cppcoreguidelines-narrowing-conversions.PedanticMode -- cgit v1.2.3 From 5d29c23eb82d440163afffe3fd8f7f600149fd64 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 20 Jun 2022 18:21:04 +0200 Subject: CMakeLists: suppress subobject-linkage warnings GCC (even 12.x) doesn't like when a template parameter is of a pointer/reference type and dumps this warning. See also: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90670 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 048d3b07..a7a80a73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ if (MSVC) /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() foreach (FLAG Wall Wpedantic Wextra Werror=return-type Wno-unused-parameter - Wno-gnu-zero-variadic-macro-arguments) + Wno-gnu-zero-variadic-macro-arguments Wno-subobject-linkage) CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) if ( COMPILER_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") -- cgit v1.2.3 From 1fa81ac5dc9259b7368e0487e0424c76bc87053e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 21 Jun 2022 14:42:59 +0200 Subject: Fix a few clang-tidy/GCC warnings --- lib/database.cpp | 10 +++++----- lib/events/accountdataevents.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 193ff54e..ed7bd794 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -28,10 +28,10 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa database().open(); switch(version()) { - case 0: migrateTo1(); - case 1: migrateTo2(); - case 2: migrateTo3(); - case 3: migrateTo4(); + case 0: migrateTo1(); [[fallthrough]]; + case 1: migrateTo2(); [[fallthrough]]; + case 2: migrateTo3(); [[fallthrough]]; + case 3: migrateTo4(); } } @@ -39,7 +39,7 @@ int Database::version() { auto query = execute(QStringLiteral("PRAGMA user_version;")); if (query.next()) { - bool ok; + bool ok = false; int value = query.value(0).toInt(&ok); qCDebug(DATABASE) << "Database version" << value; if (ok) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index ec2f64e3..24c3353c 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -32,7 +32,7 @@ struct JsonObjectConverter { if (orderJv.isDouble()) rec.order = fromJson(orderJv); if (orderJv.isString()) { - bool ok; + bool ok = false; rec.order = orderJv.toString().toFloat(&ok); if (!ok) rec.order = none; -- cgit v1.2.3 From b7f1c721fbe5cf7d56b041c0249b9d71600a24c4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 02:09:45 +0200 Subject: More .clang-tidy tweaks --- .clang-tidy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 488f3cc8..91f5a707 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' +Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,-misc-definitions-in-headers,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' WarningsAsErrors: '' HeaderFilterRegex: '' AnalyzeTemporaryDtors: false @@ -11,6 +11,8 @@ CheckOptions: value: '1' - key: bugprone-assert-side-effect.AssertMacros value: assert,NSAssert,NSCAssert,Q_ASSERT,Q_ASSERT_X +# - key: bugprone-assert-side-effect.IgnoredFunctions +# value: '' - key: bugprone-assert-side-effect.CheckFunctionCalls value: 'true' # - key: bugprone-dangling-handle.HandleClasses @@ -69,8 +71,6 @@ CheckOptions: value: '1' - key: google-readability-namespace-comments.ShortNamespaceLines value: '25' -# - key: misc-definitions-in-headers.HeaderFileExtensions -# value: ';h;hh;hpp;hxx' - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic value: 'true' # - key: misc-non-private-member-variables-in-classes.IgnorePublicMemberVariables -- cgit v1.2.3 From 04db5b6d853f3084660612c1d2db91d54ec63691 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 02:15:51 +0200 Subject: Use fixed width types for enums --- lib/quotient_common.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/quotient_common.h b/lib/quotient_common.h index e087e7d3..233bcaa1 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -51,7 +51,7 @@ Q_NAMESPACE_EXPORT(QUOTIENT_API) //! These are used for member events. The names here are case-insensitively //! equal to state names used on the wire. //! \sa MemberEventContent, RoomMemberEvent -enum class Membership : unsigned int { +enum class Membership : uint16_t { // Specific power-of-2 values (1,2,4,...) are important here as syncdata.cpp // depends on that, as well as Join being the first in line Invalid = 0x0, @@ -97,7 +97,7 @@ Q_ENUM_NS(RunningPolicy) //! \brief The result of URI resolution using UriResolver //! \sa UriResolver -enum UriResolveResult : short { +enum UriResolveResult : int8_t { StillResolving = -1, UriResolved = 0, CouldNotResolve, -- cgit v1.2.3 From 610631675826bb572bff97ce7d16d07097f14e3f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 20 Jun 2022 18:16:19 +0200 Subject: Clean up RequestData from Sonar warnings Also: make ImplPtr more flexible. --- lib/jobs/requestdata.cpp | 6 ++---- lib/jobs/requestdata.h | 22 +++++++++------------- lib/util.h | 29 +++++++++++++++++++---------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 2c001ccc..ab249f6d 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -14,7 +14,7 @@ using namespace Quotient; auto fromData(const QByteArray& data) { - auto source = std::make_unique(); + auto source = makeImpl(); source->setData(data); source->open(QIODevice::ReadOnly); return source; @@ -33,7 +33,5 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} RequestData::RequestData(QIODevice* source) - : _source(std::unique_ptr(source)) + : _source(acquireImpl(source)) {} - -RequestData::~RequestData() = default; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 41ad833a..accc8f71 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -3,11 +3,7 @@ #pragma once -#include "quotient_export.h" - -#include - -#include +#include "util.h" class QJsonObject; class QJsonArray; @@ -23,17 +19,17 @@ namespace Quotient { */ class QUOTIENT_API RequestData { public: - RequestData(const QByteArray& a = {}); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(QIODevice* source); - RequestData(RequestData&&) = default; - RequestData& operator=(RequestData&&) = default; - ~RequestData(); + // NOLINTBEGIN(google-explicit-constructor): that check should learn about + // explicit(false) + QUO_IMPLICIT RequestData(const QByteArray& a = {}); + QUO_IMPLICIT RequestData(const QJsonObject& jo); + QUO_IMPLICIT RequestData(const QJsonArray& ja); + QUO_IMPLICIT RequestData(QIODevice* source); + // NOLINTEND(google-explicit-constructor) QIODevice* source() const { return _source.get(); } private: - std::unique_ptr _source; + ImplPtr _source; }; } // namespace Quotient diff --git a/lib/util.h b/lib/util.h index 8a30f457..46b1767e 100644 --- a/lib/util.h +++ b/lib/util.h @@ -135,8 +135,8 @@ inline std::pair findFirstOf(InputIt first, InputIt last, //! to define default constructors/operator=() out of line. //! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html //! for inspiration -template -using ImplPtr = std::unique_ptr; +template +using ImplPtr = std::unique_ptr; // Why this works (see also the link above): because this defers the moment // of requiring sizeof of ImplType to the place where makeImpl is invoked @@ -156,18 +156,27 @@ using ImplPtr = std::unique_ptr; //! //! Since std::make_unique is not compatible with ImplPtr, this should be used //! in constructors of frontend classes to create implementation instances. -template -inline ImplPtr makeImpl(ArgTs&&... args) +template +inline ImplPtr makeImpl(ArgTs&&... args) { - return ImplPtr { new ImplType(std::forward(args)...), - [](ImplType* impl) { delete impl; } }; + return ImplPtr { + new ImplType(std::forward(args)...), + [](TypeToDelete* impl) { delete impl; } + }; } -template -constexpr ImplPtr ZeroImpl() +template +inline ImplPtr acquireImpl(ImplType* from) { - return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; + return ImplPtr { from, [](TypeToDelete* impl) { + delete impl; + } }; +} + +template +constexpr ImplPtr ZeroImpl() +{ + return { nullptr, [](TypeToDelete*) { /* nullptr doesn't need deletion */ } }; } //! \brief Multiplex several functors in one -- cgit v1.2.3 From 6a2cec476b72d44ecf1cd05e47724d325a46f246 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 10:17:26 +0200 Subject: Address a few more Sonar warnings --- lib/events/eventcontent.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index ea240122..1e2f3615 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -36,6 +36,10 @@ namespace EventContent { public: QJsonObject originalJson; + // You can't assign those classes + Base& operator=(const Base&) = delete; + Base& operator=(Base&&) = delete; + protected: Base(const Base&) = default; Base(Base&&) = default; @@ -145,8 +149,8 @@ namespace EventContent { class QUOTIENT_API Thumbnail : public ImageInfo { public: using ImageInfo::ImageInfo; - Thumbnail(const QJsonObject& infoJson, - const Omittable& efm = none); + explicit Thumbnail(const QJsonObject& infoJson, + const Omittable& efm = none); //! \brief Add thumbnail information to the passed `info` JSON object void dumpTo(QJsonObject& infoJson) const; -- cgit v1.2.3 From 6a7e4f883ec22ef26c1d10ba1544b0afd1a0f4d2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 20:43:04 +0200 Subject: Streamline RoomPowerLevelsEvent backoffice code Also: leave a link at the place in the spec with power level defaults to make it clear they are not invented out of thin air. --- lib/events/roompowerlevelsevent.cpp | 38 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp index 84a31d55..d9bd010b 100644 --- a/lib/events/roompowerlevelsevent.cpp +++ b/lib/events/roompowerlevelsevent.cpp @@ -5,6 +5,8 @@ using namespace Quotient; +// The default values used below are defined in +// https://spec.matrix.org/v1.3/client-server-api/#mroompower_levels PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : invite(json["invite"_ls].toInt(50)), kick(json["kick"_ls].toInt(50)), @@ -16,8 +18,7 @@ PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : users(fromJson>(json["users"_ls])), usersDefault(json["users_default"_ls].toInt(0)), notifications(Notifications{json["notifications"_ls].toObject()["room"_ls].toInt(50)}) -{ -} +{} QJsonObject PowerLevelsEventContent::toJson() const { @@ -36,32 +37,17 @@ QJsonObject PowerLevelsEventContent::toJson() const return o; } -int RoomPowerLevelsEvent::powerLevelForEvent(const QString &eventId) const { - auto e = events(); - - if (e.contains(eventId)) { - return e[eventId]; - } - - return eventsDefault(); +int RoomPowerLevelsEvent::powerLevelForEvent(const QString& eventId) const +{ + return events().value(eventId, eventsDefault()); } -int RoomPowerLevelsEvent::powerLevelForState(const QString &eventId) const { - auto e = events(); - - if (e.contains(eventId)) { - return e[eventId]; - } - - return stateDefault(); +int RoomPowerLevelsEvent::powerLevelForState(const QString& eventId) const +{ + return events().value(eventId, stateDefault()); } -int RoomPowerLevelsEvent::powerLevelForUser(const QString &userId) const { - auto u = users(); - - if (u.contains(userId)) { - return u[userId]; - } - - return usersDefault(); +int RoomPowerLevelsEvent::powerLevelForUser(const QString& userId) const +{ + return users().value(userId, usersDefault()); } -- cgit v1.2.3 From 796d78dccef1a32e35f7d24feb83cd846c55e2d2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 20:00:00 +0200 Subject: RoomSummary::merge(): explicitly cast between int and bool Honestly, it was quite intuitive even without that, but in reality there are implicit conversion under the wraps. This commit makes them explicit, for clarity. --- lib/syncdata.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index ff3dc0c2..93416bc4 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -18,9 +18,10 @@ bool RoomSummary::isEmpty() const bool RoomSummary::merge(const RoomSummary& other) { // Using bitwise OR to prevent computation shortcut. - return joinedMemberCount.merge(other.joinedMemberCount) - | invitedMemberCount.merge(other.invitedMemberCount) - | heroes.merge(other.heroes); + return static_cast( + static_cast(joinedMemberCount.merge(other.joinedMemberCount)) + | static_cast(invitedMemberCount.merge(other.invitedMemberCount)) + | static_cast(heroes.merge(other.heroes))); } QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs) -- cgit v1.2.3 From f853db5acc17f0ecd4ada65320fb2c846fc1b4d0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 21:24:30 +0200 Subject: .clang-tidy: drop another rather useless warning 'performance-no-automatic-move' triggers on code where copy elision normally takes place anyway. In fact, all cases it triggered on were also subject to named return value optimisation (NRVO). [skip ci] --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 91f5a707..d15c6eb4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,-misc-definitions-in-headers,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' +Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,-misc-definitions-in-headers,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,-performance-no-automatic-move,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' WarningsAsErrors: '' HeaderFilterRegex: '' AnalyzeTemporaryDtors: false -- cgit v1.2.3 From 5de8d0a4bc9744327703c1613fc5ac3f232f44a8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 21:36:43 +0200 Subject: Fix signature verification toJson(SignedOneTimeKey) incorrectly generated a "signatures" key mapped to an empty object when no signatures were in the C++ value. Also: fallback keys have an additional flag that also has to be taken into account when verifying signatures. --- lib/e2ee/e2ee.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 234d4bcb..aba795a4 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -89,6 +89,8 @@ public: //! Required. Signatures of the key object. //! The signature is calculated using the process described at Signing JSON. QHash> signatures; + + bool fallback = false; }; template <> @@ -97,12 +99,14 @@ struct JsonObjectConverter { { fromJson(jo.value("key"_ls), result.key); fromJson(jo.value("signatures"_ls), result.signatures); + fromJson(jo.value("fallback"_ls), result.fallback); } static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) { - addParam<>(jo, QStringLiteral("key"), result.key); - addParam<>(jo, QStringLiteral("signatures"), result.signatures); + addParam<>(jo, "key"_ls, result.key); + addParam(jo, "signatures"_ls, result.signatures); + addParam(jo, "key"_ls, result.fallback); } }; -- cgit v1.2.3 From 7576f77799c3d446d3376b0801f218a86320480d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 13:11:20 +0200 Subject: Drop QUOTIENT_API where it's undue --- lib/converters.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/converters.h b/lib/converters.h index c985fd60..385982ab 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -228,7 +228,7 @@ inline QJsonValue toJson(const std::variant& v) } template -struct QUOTIENT_API JsonConverter> { +struct JsonConverter> { static std::variant load(const QJsonValue& jv) { if (jv.isString()) -- cgit v1.2.3 From 19746de7426aeb67eb90e8c48448356691e270b0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Jun 2022 10:19:14 +0200 Subject: Use QUO_CONTENT_GETTER In keyverificationevent.*, this massively shortens repetitive getter definitions; the remaining few non-trivial ones are moved to keyverificationevent.h, dropping the respective .cpp file and therefore the dedicated translation unit. In roomkeyevent.h, it's just shorter. --- CMakeLists.txt | 2 +- lib/events/keyverificationevent.cpp | 164 ------------------------------------ lib/events/keyverificationevent.h | 80 +++++++++++------- lib/events/roomkeyevent.h | 6 +- 4 files changed, 55 insertions(+), 197 deletions(-) delete mode 100644 lib/events/keyverificationevent.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a7a80a73..94055b7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,7 +166,7 @@ list(APPEND lib_SRCS lib/events/encryptedevent.h lib/events/encryptedevent.cpp lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp lib/events/stickerevent.h lib/events/stickerevent.cpp - lib/events/keyverificationevent.h lib/events/keyverificationevent.cpp + lib/events/keyverificationevent.h lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp lib/jobs/requestdata.h lib/jobs/requestdata.cpp lib/jobs/basejob.h lib/jobs/basejob.cpp diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp deleted file mode 100644 index 4803955d..00000000 --- a/lib/events/keyverificationevent.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "keyverificationevent.h" - -using namespace Quotient; - -KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationRequestEvent::fromDevice() const -{ - return contentPart("from_device"_ls); -} - -QString KeyVerificationRequestEvent::transactionId() const -{ - return contentPart("transaction_id"_ls); -} - -QStringList KeyVerificationRequestEvent::methods() const -{ - return contentPart("methods"_ls); -} - -uint64_t KeyVerificationRequestEvent::timestamp() const -{ - return contentPart("timestamp"_ls); -} - -KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationStartEvent::fromDevice() const -{ - return contentPart("from_device"_ls); -} - -QString KeyVerificationStartEvent::transactionId() const -{ - return contentPart("transaction_id"_ls); -} - -QString KeyVerificationStartEvent::method() const -{ - return contentPart("method"_ls); -} - -Omittable KeyVerificationStartEvent::nextMethod() const -{ - return contentPart>("method_ls"); -} - -QStringList KeyVerificationStartEvent::keyAgreementProtocols() const -{ - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - return contentPart("key_agreement_protocols"_ls); -} - -QStringList KeyVerificationStartEvent::hashes() const -{ - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - return contentPart("hashes"_ls); - -} - -QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const -{ - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - return contentPart("message_authentication_codes"_ls); -} - -QString KeyVerificationStartEvent::shortAuthenticationString() const -{ - return contentPart("short_authentification_string"_ls); -} - -KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationAcceptEvent::transactionId() const -{ - return contentPart("transaction_id"_ls); -} - -QString KeyVerificationAcceptEvent::method() const -{ - return contentPart("method"_ls); -} - -QString KeyVerificationAcceptEvent::keyAgreementProtocol() const -{ - return contentPart("key_agreement_protocol"_ls); -} - -QString KeyVerificationAcceptEvent::hashData() const -{ - return contentPart("hash"_ls); -} - -QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const -{ - return contentPart("short_authentification_string"_ls); -} - -QString KeyVerificationAcceptEvent::commitement() const -{ - return contentPart("commitment"_ls); -} - -KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationCancelEvent::transactionId() const -{ - return contentPart("transaction_id"_ls); -} - -QString KeyVerificationCancelEvent::reason() const -{ - return contentPart("reason"_ls); -} - -QString KeyVerificationCancelEvent::code() const -{ - return contentPart("code"_ls); -} - -KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationKeyEvent::transactionId() const -{ - return contentPart("transaction_id"_ls); -} - -QString KeyVerificationKeyEvent::key() const -{ - return contentPart("key"_ls); -} - -KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationMacEvent::transactionId() const -{ - return contentPart("transaction_id"_ls); -} - -QString KeyVerificationMacEvent::keys() const -{ - return contentPart("keys"_ls); -} - -QHash KeyVerificationMacEvent::mac() const -{ - return contentPart>("mac"_ls); -} diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 497e56a2..78457e0c 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -14,20 +14,20 @@ public: explicit KeyVerificationRequestEvent(const QJsonObject& obj); /// The device ID which is initiating the request. - QString fromDevice() const; + QUO_CONTENT_GETTER(QString, fromDevice) /// An opaque identifier for the verification request. Must /// be unique with respect to the devices involved. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification methods supported by the sender. - QStringList methods() const; + QUO_CONTENT_GETTER(QStringList, methods) /// The POSIX timestamp in milliseconds for when the request was /// made. If the request is in the future by more than 5 minutes or /// more than 10 minutes in the past, the message should be ignored /// by the receiver. - uint64_t timestamp() const; + QUO_CONTENT_GETTER(uint64_t, timestamp) }; REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) @@ -39,36 +39,52 @@ public: explicit KeyVerificationStartEvent(const QJsonObject &obj); /// The device ID which is initiating the process. - QString fromDevice() const; + QUO_CONTENT_GETTER(QString, fromDevice) /// An opaque identifier for the verification request. Must /// be unique with respect to the devices involved. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification method to use. - QString method() const; + QUO_CONTENT_GETTER(QString, method) /// Optional method to use to verify the other user's key with. - Omittable nextMethod() const; + QUO_CONTENT_GETTER(Omittable, nextMethod) // SAS.V1 methods /// The key agreement protocols the sending device understands. /// \note Only exist if method is m.sas.v1 - QStringList keyAgreementProtocols() const; + QStringList keyAgreementProtocols() const + { + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart("key_agreement_protocols"_ls); + } /// The hash methods the sending device understands. /// \note Only exist if method is m.sas.v1 - QStringList hashes() const; + QStringList hashes() const + { + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart("hashes"_ls); + } /// The message authentication codes that the sending device understands. /// \note Only exist if method is m.sas.v1 - QStringList messageAuthenticationCodes() const; + QStringList messageAuthenticationCodes() const + { + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart("message_authentication_codes"_ls); + } /// The SAS methods the sending device (and the sending device's /// user) understands. /// \note Only exist if method is m.sas.v1 - QString shortAuthenticationString() const; + QString shortAuthenticationString() const + { + Q_ASSERT(method() == QStringLiteral("m.sas.v1")); + return contentPart("short_authentification_string"_ls); + } }; REGISTER_EVENT_TYPE(KeyVerificationStartEvent) @@ -81,30 +97,33 @@ public: explicit KeyVerificationAcceptEvent(const QJsonObject& obj); /// An opaque identifier for the verification process. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification method to use. Must be 'm.sas.v1'. - QString method() const; + QUO_CONTENT_GETTER(QString, method) /// The key agreement protocol the device is choosing to use, out of /// the options in the m.key.verification.start message. - QString keyAgreementProtocol() const; + QUO_CONTENT_GETTER(QString, keyAgreementProtocol) /// The hash method the device is choosing to use, out of the /// options in the m.key.verification.start message. - QString hashData() const; + QString hashData() const + { + return contentPart("hash"_ls); + } /// The message authentication code the device is choosing to use, out /// of the options in the m.key.verification.start message. - QString messageAuthenticationCode() const; + QUO_CONTENT_GETTER(QString, messageAuthenticationCode) /// The SAS methods both devices involved in the verification process understand. - QStringList shortAuthenticationString() const; + QUO_CONTENT_GETTER(QStringList, shortAuthenticationString) /// The hash (encoded as unpadded base64) of the concatenation of the /// device's ephemeral public key (encoded as unpadded base64) and the /// canonical JSON representation of the m.key.verification.start message. - QString commitement() const; + QUO_CONTENT_GETTER(QString, commitment) }; REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) @@ -115,14 +134,14 @@ public: explicit KeyVerificationCancelEvent(const QJsonObject &obj); /// An opaque identifier for the verification process. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// A human readable description of the code. The client should only /// rely on this string if it does not understand the code. - QString reason() const; + QUO_CONTENT_GETTER(QString, reason) /// The error code for why the process/request was cancelled by the user. - QString code() const; + QUO_CONTENT_GETTER(QString, code) }; REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) @@ -134,11 +153,11 @@ public: explicit KeyVerificationKeyEvent(const QJsonObject &obj); - /// An opaque identifier for the verification process. - QString transactionId() const; + /// An opaque identifier for the verification process. + QUO_CONTENT_GETTER(QString, transactionId) /// The device's ephemeral public key, encoded as unpadded base64. - QString key() const; + QUO_CONTENT_GETTER(QString, key) }; REGISTER_EVENT_TYPE(KeyVerificationKeyEvent) @@ -149,13 +168,16 @@ public: explicit KeyVerificationMacEvent(const QJsonObject &obj); - /// An opaque identifier for the verification process. - QString transactionId() const; + /// An opaque identifier for the verification process. + QUO_CONTENT_GETTER(QString, transactionId) /// The device's ephemeral public key, encoded as unpadded base64. - QString keys() const; + QUO_CONTENT_GETTER(QString, keys) - QHash mac() const; + QHash mac() const + { + return contentPart>("mac"_ls); + } }; REGISTER_EVENT_TYPE(KeyVerificationMacEvent) } // namespace Quotient diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 3093db41..9eb2854b 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -16,9 +16,9 @@ public: const QString& sessionId, const QString& sessionKey, const QString& senderId); - QString algorithm() const { return contentPart("algorithm"_ls); } - QString roomId() const { return contentPart(RoomIdKeyL); } - QString sessionId() const { return contentPart("session_id"_ls); } + QUO_CONTENT_GETTER(QString, algorithm) + QUO_CONTENT_GETTER(QString, roomId) + QUO_CONTENT_GETTER(QString, sessionId) QByteArray sessionKey() const { return contentPart("session_key"_ls).toLatin1(); -- 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/events/encryptionevent.h | 2 ++ lib/room.cpp | 30 ++++++++++-------------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index c73e5598..945b17e7 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -52,6 +52,8 @@ public: QString algorithm() const { return content().algorithm; } int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } + + bool useEncryption() const { return !algorithm().isEmpty(); } }; REGISTER_EVENT_TYPE(EncryptionEvent) } // namespace Quotient 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 9f7a65b04c246de4c27b205ece778ede1ad7df7e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Jun 2022 09:18:09 +0200 Subject: Fix copy-pasta in signed one-time key JSON dumper --- lib/connection.cpp | 5 +++-- lib/e2ee/e2ee.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2319a38a..13a35684 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2306,10 +2306,11 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, signedOneTimeKey ->signatures[targetUserId]["ed25519:"_ls % targetDeviceId] .toLatin1(); + const auto payloadObject = + toJson(SignedOneTimeKey { signedOneTimeKey->key, {} }); if (!verifier.ed25519Verify( edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), - QJsonDocument(toJson(SignedOneTimeKey { signedOneTimeKey->key, {} })) - .toJson(QJsonDocument::Compact), + QJsonDocument(payloadObject).toJson(QJsonDocument::Compact), signature)) { qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId << targetDeviceId << ". Skipping this device."; diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index aba795a4..17c87f53 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -106,7 +106,7 @@ struct JsonObjectConverter { { addParam<>(jo, "key"_ls, result.key); addParam(jo, "signatures"_ls, result.signatures); - addParam(jo, "key"_ls, result.fallback); + addParam(jo, "fallback"_ls, result.fallback); } }; -- cgit v1.2.3 From 6ae41d68dcdb91e5ec4a3ea48a151daaa0765765 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Jun 2022 15:10:33 +0200 Subject: Rework SignedOneTimeKey as a QJsonObject wrapper Since this object has to be verified against a signature it also carries there's a rather specific procedure described in The Spec for that. That procedure basically assumes handling the signed one-time key object as a JSON object, not as a C++ object. And originally Quotient E2EE code was exactly like that (obtaining the right QJsonObject from the job result and handling it as specced) but then one enthusiastic developer (me) decided it's better to use a proper C++ structure - breaking the verification logic along the way. After a couple attempts to fix it, here we are again: SignedOneTimeKey is a proper QJsonObject, and even provides a method returning its JSON in the form prepared for verification (according to the spec). --- lib/connection.cpp | 10 +++----- lib/e2ee/e2ee.h | 62 ++++++++++++++++++++++++++++++++---------------- lib/e2ee/qolmaccount.cpp | 12 ++++------ 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 13a35684..690b3f6a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2303,14 +2303,10 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, // Verify contents of signedOneTimeKey - for that, drop `signatures` and // `unsigned` and then verify the object against the respective signature const auto signature = - signedOneTimeKey - ->signatures[targetUserId]["ed25519:"_ls % targetDeviceId] - .toLatin1(); - const auto payloadObject = - toJson(SignedOneTimeKey { signedOneTimeKey->key, {} }); + signedOneTimeKey->signature(targetUserId, targetDeviceId); if (!verifier.ed25519Verify( edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), - QJsonDocument(payloadObject).toJson(QJsonDocument::Compact), + signedOneTimeKey->toJsonForVerification(), signature)) { qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId << targetDeviceId << ". Skipping this device."; @@ -2320,7 +2316,7 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, curveKeyForUserDevice(targetUserId, targetDeviceId); auto session = QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, - signedOneTimeKey->key); + signedOneTimeKey->key()); if (!session) { qCWarning(E2EE) << "Failed to create olm session for " << recipientCurveKey << session.error(); diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 17c87f53..7b9b5820 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -10,8 +10,10 @@ #include "qolmerrors.h" #include -#include +#include + #include +#include namespace Quotient { @@ -79,35 +81,53 @@ struct UnsignedOneTimeKeys QHash curve25519() const { return keys[Curve25519Key]; } }; -//! Struct representing the signed one-time keys. -class SignedOneTimeKey -{ +class SignedOneTimeKey { public: - //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. - QString key; + explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId, + const QString& deviceId, const QString& signature) + : payload { { "key"_ls, unsignedKey }, + { "signatures"_ls, + QJsonObject { + { userId, QJsonObject { { "ed25519:"_ls % deviceId, + signature } } } } } } + {} + explicit SignedOneTimeKey(const QJsonObject& jo = {}) + : payload(jo) + {} - //! Required. Signatures of the key object. - //! The signature is calculated using the process described at Signing JSON. - QHash> signatures; + //! Unpadded Base64-encoded 32-byte Curve25519 public key + QString key() const { return payload["key"_ls].toString(); } - bool fallback = false; -}; + //! \brief Signatures of the key object + //! + //! The signature is calculated using the process described at + //! https://spec.matrix.org/v1.3/appendices/#signing-json + auto signatures() const + { + return fromJson>>( + payload["signatures"_ls]); + } -template <> -struct JsonObjectConverter { - static void fillFrom(const QJsonObject& jo, SignedOneTimeKey& result) + QByteArray signature(QStringView userId, QStringView deviceId) const { - fromJson(jo.value("key"_ls), result.key); - fromJson(jo.value("signatures"_ls), result.signatures); - fromJson(jo.value("fallback"_ls), result.fallback); + return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId] + .toString() + .toLatin1(); } - static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) + //! Whether the key is a fallback key + bool isFallback() const { return payload["fallback"_ls].toBool(); } + auto toJson() const { return payload; } + auto toJsonForVerification() const { - addParam<>(jo, "key"_ls, result.key); - addParam(jo, "signatures"_ls, result.signatures); - addParam(jo, "fallback"_ls, result.fallback); + auto json = payload; + json.remove("signatures"_ls); + json.remove("unsigned"_ls); + return QJsonDocument(json).toJson(QJsonDocument::Compact); } + +private: + QJsonObject payload; }; using OneTimeKeys = QHash>; diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 241ae750..c3714363 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -162,17 +162,13 @@ OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const OneTimeKeys signedOneTimeKeys; for (const auto& curveKeys = keys.curve25519(); const auto& [keyId, key] : asKeyValueRange(curveKeys)) - signedOneTimeKeys["signed_curve25519:" % keyId] = - signedOneTimeKey(key.toUtf8(), sign(QJsonObject{{"key", key}})); + signedOneTimeKeys.insert("signed_curve25519:" % keyId, + SignedOneTimeKey { + key, m_userId, m_deviceId, + sign(QJsonObject { { "key", key } }) }); return signedOneTimeKeys; } -SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray& key, - const QString& signature) const -{ - return { key, { { m_userId, { { "ed25519:" + m_deviceId, signature } } } } }; -} - std::optional QOlmAccount::removeOneTimeKeys( const QOlmSession& session) { -- cgit v1.2.3 From 7fdb1a8653863f580b2672faefc08fb372258df8 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Jun 2022 15:56:03 +0200 Subject: Code cleanup and reformatting --- lib/connection.cpp | 5 +++-- lib/converters.h | 4 ++-- lib/e2ee/qolmaccount.cpp | 6 ++++-- lib/e2ee/qolmaccount.h | 2 -- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 690b3f6a..701f78c2 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2308,8 +2308,9 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), signedOneTimeKey->toJsonForVerification(), signature)) { - qWarning(E2EE) << "Failed to verify one-time-key signature for" << targetUserId - << targetDeviceId << ". Skipping this device."; + qWarning(E2EE) << "Failed to verify one-time-key signature for" + << targetUserId << targetDeviceId + << ". Skipping this device."; return false; } const auto recipientCurveKey = diff --git a/lib/converters.h b/lib/converters.h index 385982ab..c445442c 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -86,8 +86,8 @@ struct JsonConverter : _impl::JsonExporter { static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } }; -template >> +template + requires (!std::is_constructible_v) inline auto toJson(const T& pod) // -> can return anything from which QJsonValue or, in some cases, QJsonDocument // is constructible diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index c3714363..cd10f165 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -145,9 +145,11 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const { const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); - QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); + QByteArray oneTimeKeysBuffer(static_cast(oneTimeKeyLength), '0'); - const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); + const auto error = olm_account_one_time_keys(m_account, + oneTimeKeysBuffer.data(), + oneTimeKeyLength); if (error == olm_error()) { throw lastError(m_account); } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 23fe58dd..f2a31314 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -64,8 +64,6 @@ public: //! Sign all one time keys. OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const; - SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; - UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const; DeviceKeys deviceKeys() const; -- cgit v1.2.3 From 4d4d363b29ff4e471511ff454a58d7d8b88d215d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Jun 2022 15:54:36 +0200 Subject: Start using C++20's designated initializers --- lib/e2ee/e2ee.h | 6 +++--- lib/e2ee/qolmaccount.cpp | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 7b9b5820..449e6ef7 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -37,11 +37,11 @@ constexpr auto SignedCurve25519Key = "signed_curve25519"_ls; constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls; constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; +constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; + inline bool isSupportedAlgorithm(const QString& algorithm) { - static constexpr std::array SupportedAlgorithms { - OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey - }; return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), algorithm) != SupportedAlgorithms.cend(); diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index cd10f165..ccb191f4 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -187,19 +187,19 @@ OlmAccount* QOlmAccount::data() { return m_account; } DeviceKeys QOlmAccount::deviceKeys() const { - DeviceKeys deviceKeys; - deviceKeys.userId = m_userId; - deviceKeys.deviceId = m_deviceId; - deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}; + static QStringList Algorithms(SupportedAlgorithms.cbegin(), + SupportedAlgorithms.cend()); const auto idKeys = identityKeys(); - deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519; - deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519; - - const auto sign = signIdentityKeys(); - deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; - - return deviceKeys; + return DeviceKeys { + .userId = m_userId, + .deviceId = m_deviceId, + .algorithms = Algorithms, + .keys { { "curve25519:" + m_deviceId, idKeys.curve25519 }, + { "ed25519:" + m_deviceId, idKeys.ed25519 } }, + .signatures { + { m_userId, { { "ed25519:" + m_deviceId, signIdentityKeys() } } } } + }; } UploadKeysJob* QOlmAccount::createUploadKeyRequest( -- cgit v1.2.3 From 63e4cce8cc32af9bd92ead9876a3642d7cbdfb31 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Jun 2022 22:41:15 +0200 Subject: Fix the just introduced Sonar warning Too many parameters of the same type in a row. --- lib/e2ee/e2ee.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 449e6ef7..0772b70a 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -84,12 +84,13 @@ struct UnsignedOneTimeKeys class SignedOneTimeKey { public: explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId, - const QString& deviceId, const QString& signature) + const QString& deviceId, + const QByteArray& signature) : payload { { "key"_ls, unsignedKey }, { "signatures"_ls, QJsonObject { { userId, QJsonObject { { "ed25519:"_ls % deviceId, - signature } } } } } } + QString(signature) } } } } } } {} explicit SignedOneTimeKey(const QJsonObject& jo = {}) : payload(jo) -- cgit v1.2.3 From 8580043a64941e1c1f132737e4b50280ac17812a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 24 Jun 2022 23:11:20 +0200 Subject: Make EventContent::Base() move constructor noexcept --- lib/events/eventcontent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 1e2f3615..7611d077 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -42,7 +42,7 @@ namespace EventContent { protected: Base(const Base&) = default; - Base(Base&&) = default; + Base(Base&&) noexcept = default; virtual void fillJson(QJsonObject&) const = 0; }; -- cgit v1.2.3 From bc8e01df4286f5f8ff9103fbebad801f355db689 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 3 Jul 2022 22:14:13 +0200 Subject: Shorten switchOnType, function_traits and connect* ...thanks to C++20 awesomeness. A notable change is that wrap_in_function() (and respectively function_traits<>::function_type) and fn_return_t alias are gone. The former are no more needed because connectUntil/connectSingleShot no more use std::function. The latter has been relatively underused and with the optimisation of switchOnType hereby, could be completely replaced with std::invoke_result_t. Rewriting connect* functions using constexpr and auto parameters made the implementation 30% more compact and much easier to understand (though still with a couple of - now thoroughly commented - tricky places). Dropping std::function<> from it may also bring some (quite modest, likely) performance benefits. --- lib/events/event.h | 64 +++++++------------ lib/function_traits.cpp | 3 + lib/function_traits.h | 36 +++++------ lib/qt_connection_util.h | 160 +++++++++++++++++------------------------------ 4 files changed, 101 insertions(+), 162 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index ec21c6aa..a966e613 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -341,56 +341,34 @@ inline auto eventCast(const BasePtrT& eptr) : nullptr; } -// A trivial generic catch-all "switch" -template -inline auto switchOnType(const BaseEventT& event, FnT&& fn) - -> decltype(fn(event)) -{ - return fn(event); -} - namespace _impl { - // Using bool instead of auto below because auto apparently upsets MSVC - template - constexpr bool needs_downcast = - std::is_base_of_v>> - && !std::is_same_v>>; -} - -// A trivial type-specific "switch" for a void function -template -inline auto switchOnType(const BaseT& event, FnT&& fn) - -> std::enable_if_t<_impl::needs_downcast - && std::is_void_v>> -{ - using event_type = fn_arg_t; - if (is>(event)) - fn(static_cast(event)); + template + concept Invocable_With_Downcast = + std::derived_from>, BaseT>; } -// A trivial type-specific "switch" for non-void functions with an optional -// default value; non-voidness is guarded by defaultValue type -template -inline auto switchOnType(const BaseT& event, FnT&& fn, - fn_return_t&& defaultValue = {}) - -> std::enable_if_t<_impl::needs_downcast, fn_return_t> +template +inline auto switchOnType(const BaseT& event, TailT&& tail) { - using event_type = fn_arg_t; - if (is>(event)) - return fn(static_cast(event)); - return std::move(defaultValue); + if constexpr (std::is_invocable_v) { + return tail(event); + } else if constexpr (_impl::Invocable_With_Downcast) { + using event_type = fn_arg_t; + if (is>(event)) + return tail(static_cast(event)); + return std::invoke_result_t(); // Default-constructed + } else { // Treat it as a value to return + return std::forward(tail); + } } -// A switch for a chain of 2 or more functions -template -inline std::common_type_t, fn_return_t> -switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) +template +inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) { using event_type1 = fn_arg_t; if (is>(event)) - return fn1(static_cast(event)); - return switchOnType(event, std::forward(fn2), - std::forward(fns)...); + return fn1(static_cast(event)); + return switchOnType(event, std::forward(fns)...); } template @@ -405,8 +383,8 @@ inline auto visit(const BaseT& event, FnTs&&... fns) // TODO: replace with ranges::for_each once all standard libraries have it template inline auto visitEach(RangeT&& events, FnTs&&... fns) - -> std::enable_if_t(fns)...))>> + requires std::is_void_v< + decltype(switchOnType(**begin(events), std::forward(fns)...))> { for (auto&& evtPtr: events) switchOnType(*evtPtr, std::forward(fns)...); diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp index 6542101a..e3d27122 100644 --- a/lib/function_traits.cpp +++ b/lib/function_traits.cpp @@ -7,6 +7,9 @@ using namespace Quotient; +template +using fn_return_t = typename function_traits::return_type; + int f_(); static_assert(std::is_same_v, int>, "Test fn_return_t<>"); diff --git a/lib/function_traits.h b/lib/function_traits.h index 83b8e425..143ed162 100644 --- a/lib/function_traits.h +++ b/lib/function_traits.h @@ -8,7 +8,7 @@ namespace Quotient { namespace _impl { - template + template struct fn_traits {}; } @@ -21,73 +21,73 @@ namespace _impl { */ template struct function_traits - : public _impl::fn_traits> {}; + : public _impl::fn_traits> {}; // Specialisation for a function template struct function_traits { using return_type = ReturnT; using arg_types = std::tuple; - // See also the comment for wrap_in_function() in qt_connection_util.h - using function_type = std::function; }; namespace _impl { - template + template struct fn_object_traits; // Specialisation for a lambda function template - struct fn_object_traits + struct fn_object_traits : function_traits {}; // Specialisation for a const lambda function template - struct fn_object_traits + struct fn_object_traits : function_traits {}; // Specialisation for function objects with (non-overloaded) operator() // (this includes non-generic lambdas) template - struct fn_traits - : public fn_object_traits {}; + requires requires { &T::operator(); } + struct fn_traits + : public fn_object_traits {}; // Specialisation for a member function in a non-functor class template - struct fn_traits + struct fn_traits : function_traits {}; // Specialisation for a const member function template - struct fn_traits + struct fn_traits : function_traits {}; // Specialisation for a constref member function template - struct fn_traits + struct fn_traits : function_traits {}; // Specialisation for a prvalue member function template - struct fn_traits + struct fn_traits : function_traits {}; // Specialisation for a pointer-to-member template - struct fn_traits + struct fn_traits : function_traits {}; // Specialisation for a const pointer-to-member template - struct fn_traits + struct fn_traits : function_traits {}; } // namespace _impl -template -using fn_return_t = typename function_traits::return_type; - template using fn_arg_t = std::tuple_element_t::arg_types>; +template +constexpr auto fn_arg_count_v = + std::tuple_size_v::arg_types>; + } // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 86593cc8..7477273f 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -9,101 +9,68 @@ namespace Quotient { namespace _impl { - template - using decorated_slot_tt = - std::function; + enum ConnectionType { SingleShot, Until }; - template - inline QMetaObject::Connection - connectDecorated(SenderT* sender, SignalT signal, ContextT* context, - decorated_slot_tt decoratedSlot, - Qt::ConnectionType connType) + template + inline auto connect(auto* sender, auto signal, auto* context, auto slotLike, + Qt::ConnectionType connType) { - auto pc = std::make_unique(); - auto& c = *pc; // Resolve a reference before pc is moved to lambda - - // Perfect forwarding doesn't work through signal-slot connections - - // arguments are always copied (at best - COWed) to the context of - // the slot. Therefore the slot decorator receives const ArgTs&... - // rather than ArgTs&&... - // TODO (C++20): std::bind_front() instead of lambda. - c = QObject::connect(sender, signal, context, - [pc = std::move(pc), - decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) { - Q_ASSERT(*pc); // If it's been triggered, it should exist - decoratedSlot(*pc, args...); + std::unique_ptr pConn = + std::make_unique(); + auto& c = *pConn; // Save the reference before pConn is moved from + c = QObject::connect( + sender, signal, context, + [slotLike, pConn = std::move(pConn)](const auto&... args) + // The requires-expression below is necessary to prevent Qt + // from eagerly trying to fill the lambda with more arguments + // than slotLike() (i.e., the original slot) can handle + requires requires { slotLike(args...); } { + static_assert(CType == Until || CType == SingleShot, + "Unsupported disconnection type"); + if constexpr (CType == SingleShot) { + // Disconnect early to avoid re-triggers during slotLike() + QObject::disconnect(*pConn); + // Qt kindly keeps slot objects until they do their job, + // even if they disconnect themselves in the process (see + // how doActivate() in qobject.cpp handles c->slotObj). + slotLike(args...); + } else if constexpr (CType == Until) { + if (slotLike(args...)) + QObject::disconnect(*pConn); + } }, connType); return c; } - template - inline QMetaObject::Connection - connectUntil(SenderT* sender, SignalT signal, ContextT* context, - std::function functor, - Qt::ConnectionType connType) - { - return connectDecorated(sender, signal, context, - decorated_slot_tt( - [functor = std::move(functor)](QMetaObject::Connection& c, - const ArgTs&... args) { - if (functor(args...)) - QObject::disconnect(c); - }), - connType); - } - template - inline QMetaObject::Connection - connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, - std::function slot, - Qt::ConnectionType connType) - { - return connectDecorated(sender, signal, context, - decorated_slot_tt( - [slot = std::move(slot)](QMetaObject::Connection& c, - const ArgTs&... args) { - QObject::disconnect(c); - slot(args...); - }), - connType); - } - // TODO: get rid of it as soon as Apple Clang gets proper deduction guides - // for std::function<> - // ...or consider using QtPrivate magic used by QObject::connect() - // ...for inspiration, also check a possible std::not_fn implementation - // at https://en.cppreference.com/w/cpp/utility/functional/not_fn - template - inline auto wrap_in_function(FnT&& f) - { - return typename function_traits::function_type(std::forward(f)); - } + template + concept PmfSlot = + (fn_arg_count_v > 0 + && std::derived_from>>); } // namespace _impl -/*! \brief Create a connection that self-disconnects when its "slot" returns true - * - * A slot accepted by connectUntil() is different from classic Qt slots - * in that its return value must be bool, not void. The slot's return value - * controls whether the connection should be kept; if the slot returns false, - * the connection remains; upon returning true, the slot is disconnected from - * the signal. Because of a different slot signature connectUntil() doesn't - * accept member functions as QObject::connect or Quotient::connectSingleShot - * do; you should pass a lambda or a pre-bound member function to it. - */ -template -inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, - const FunctorT& slot, +//! \brief Create a connection that self-disconnects when its slot returns true +//! +//! A slot accepted by connectUntil() is different from classic Qt slots +//! in that its return value must be bool, not void. Because of that different +//! signature connectUntil() doesn't accept member functions in the way +//! QObject::connect or Quotient::connectSingleShot do; you should pass a lambda +//! or a pre-bound member function to it. +//! \return whether the connection should be dropped; false means that the +//! connection remains; upon returning true, the slot is disconnected +//! from the signal. +inline auto connectUntil(auto* sender, auto signal, auto* context, + auto smartSlot, Qt::ConnectionType connType = Qt::AutoConnection) { - return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot), - connType); + return _impl::connect<_impl::Until>(sender, signal, context, smartSlot, + connType); } -/// Create a connection that self-disconnects after triggering on the signal -template -inline auto connectSingleShot(SenderT* sender, SignalT signal, - ContextT* context, const FunctorT& slot, +//! Create a connection that self-disconnects after triggering on the signal +template +inline auto connectSingleShot(auto* sender, auto signal, ContextT* context, + SlotT slot, Qt::ConnectionType connType = Qt::AutoConnection) { #if QT_VERSION_MAJOR >= 6 @@ -111,25 +78,16 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, Qt::ConnectionType(connType | Qt::SingleShotConnection)); #else - return _impl::connectSingleShot( - sender, signal, context, _impl::wrap_in_function(slot), connType); -} - -// Specialisation for usual Qt slots passed as pointers-to-members. -template -inline auto connectSingleShot(SenderT* sender, SignalT signal, - ReceiverT* receiver, - void (SlotObjectT::*slot)(ArgTs...), - Qt::ConnectionType connType = Qt::AutoConnection) -{ - // TODO: when switching to C++20, use std::bind_front() instead - return _impl::connectSingleShot(sender, signal, receiver, - _impl::wrap_in_function( - [receiver, slot](const ArgTs&... args) { - (receiver->*slot)(args...); - }), - connType); + // In case for usual Qt slots passed as pointers-to-members the receiver + // object has to be pre-bound to the slot to make it self-contained + if constexpr (_impl::PmfSlot) { + return _impl::connect<_impl::SingleShot>(sender, signal, context, + std::bind_front(slot, context), + connType); + } else { + return _impl::connect<_impl::SingleShot>(sender, signal, context, slot, + connType); + } #endif } -- cgit v1.2.3 From 64948b6840032b04ef00bfe207baa29dd445d141 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 7 Jul 2022 18:32:48 +0200 Subject: Avoid std::derived_from and std::bind_front Apple Clang doesn't have those yet. --- lib/events/event.h | 2 +- lib/qt_connection_util.h | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index a966e613..05eb51e9 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -344,7 +344,7 @@ inline auto eventCast(const BasePtrT& eptr) namespace _impl { template concept Invocable_With_Downcast = - std::derived_from>, BaseT>; + std::is_base_of_v>>; } template diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 7477273f..edcdc572 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -46,7 +46,7 @@ namespace _impl { template concept PmfSlot = (fn_arg_count_v > 0 - && std::derived_from>>); + && std::is_base_of_v>, ReceiverT>); } // namespace _impl //! \brief Create a connection that self-disconnects when its slot returns true @@ -78,12 +78,21 @@ inline auto connectSingleShot(auto* sender, auto signal, ContextT* context, Qt::ConnectionType(connType | Qt::SingleShotConnection)); #else - // In case for usual Qt slots passed as pointers-to-members the receiver + // In case of classic Qt pointer-to-member-function slots the receiver // object has to be pre-bound to the slot to make it self-contained if constexpr (_impl::PmfSlot) { + auto&& boundSlot = +# if __cpp_lib_bind_front // Needs Apple Clang 13 (other platforms are fine) + std::bind_front(slot, context); +# else + [context, slot](const auto&... args) + requires requires { (context->*slot)(args...); } + { + (context->*slot)(args...); + }; +# endif return _impl::connect<_impl::SingleShot>(sender, signal, context, - std::bind_front(slot, context), - connType); + std::move(boundSlot), connType); } else { return _impl::connect<_impl::SingleShot>(sender, signal, context, slot, connType); -- cgit v1.2.3 From 955e1314ebfd83d6f44d88547159e6492035681e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 8 Jul 2022 09:28:58 +0200 Subject: CI: use GCC 11 and (therefore) ubuntu-22.04 GCC 10 ICE's[1] in qt_connection_util.h code; and ubuntu-20.04 doesn't have GCC 11. Also: patch a Qt 5.15 header when compiling with GCC because a combination of Qt 5.15 and GCC 11 in turn triggers QTBUG-91909/90568... Which in turn required moving Qt setup before the build environment setup. Life's fun. [1] Internal Compiler Error --- .github/workflows/ci.yml | 41 +++++++++++++++++++++++------------------ README.md | 41 ++++++++++++++++++++++++++++++++--------- lib/qt_connection_util.h | 5 +++-- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d619385f..f84356b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,12 +47,12 @@ jobs: compiler: LLVM e2ee: e2ee static-analysis: codeql - - os: ubuntu-latest + - os: ubuntu-22.04 qt-version: '5.15.2' compiler: GCC e2ee: e2ee static-analysis: sonar - - os: ubuntu-20.04 + - os: ubuntu-22.04 qt-version: '5.15.2' compiler: GCC e2ee: e2ee @@ -82,11 +82,30 @@ jobs: with: fetch-depth: 0 + - name: Cache Qt + id: cache-qt + uses: actions/cache@v2 + with: + path: ${{ runner.workspace }}/Qt + key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache + + - name: Install Qt + uses: jurplel/install-qt-action@v2.14.0 + with: + version: ${{ matrix.qt-version }} + arch: ${{ matrix.qt-arch }} + cached: ${{ steps.cache-qt.outputs.cache-hit }} + - name: Setup build environment run: | if [ '${{ matrix.compiler }}' == 'GCC' ]; then - echo "CC=gcc-10" >>$GITHUB_ENV - echo "CXX=g++-10" >>$GITHUB_ENV + echo "CC=gcc" >>$GITHUB_ENV + echo "CXX=g++" >>$GITHUB_ENV + if [ '${{ startsWith(matrix.qt-version, '5') }}' == 'true' ]; then + # Patch Qt to avoid GCC tumbling over QTBUG-90568/QTBUG-91909 + sed -i 's/ThreadEngineStarter(ThreadEngine \*_threadEngine)/ThreadEngineStarter(ThreadEngine \*_threadEngine)/' \ + $Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h + fi elif [[ '${{ runner.os }}' != 'Windows' ]]; then echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV @@ -124,20 +143,6 @@ jobs: cmake -E make_directory ${{ runner.workspace }}/build echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV - - name: Cache Qt - id: cache-qt - uses: actions/cache@v2 - with: - path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache - - - name: Install Qt - uses: jurplel/install-qt-action@v2.14.0 - with: - version: ${{ matrix.qt-version }} - arch: ${{ matrix.qt-arch }} - cached: ${{ steps.cache-qt.outputs.cache-hit }} - - name: Install Ninja (macOS/Windows) if: ${{ !startsWith(matrix.os, 'ubuntu') }} uses: seanmiddleditch/gha-setup-ninja@v3 diff --git a/README.md b/README.md index e0f4596c..8be38b5c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ your application, as described below. - CMake 3.16 or newer (from your package management system or [the official website](https://cmake.org/download/)) - A C++ toolchain with that supports at least some subset of C++20: - - GCC 10 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) + - GCC 11 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) and Visual Studio 2019 (Windows) are the oldest officially supported. - Any build system that works with CMake should be fine: GNU Make and ninja on any platform, NMake and jom on Windows are known to work. @@ -165,14 +165,37 @@ by setting `Quotient_INSTALL_TESTS` to `OFF`. #### Building fails -If `cmake` fails with... -``` -CMake Warning at CMakeLists.txt:11 (find_package): - By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project - has asked CMake to find a package configuration file provided by - "Qt5Widgets", but CMake did not find one. -``` -...then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. +- If `cmake` fails with + ``` + CMake Warning at CMakeLists.txt:11 (find_package): + By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project + has asked CMake to find a package configuration file provided by + "Qt5Widgets", but CMake did not find one. + ``` + then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. + +- If you use GCC and get an "unknown declarator" compilation error in the file +`qtconcurrentthreadengine.h` - unfortunately, it is an actual error in Qt 5.15 +sources, see https://bugreports.qt.io/browse/QTBUG-90568 (or +https://bugreports.qt.io/browse/QTBUG-91909). The Qt company did not make +an open source release with the fix, therefore: + + - if you're on Linux - try to use Qt from your package management system, as + most likely this bug is already fixed in the packages + - if you're on Windows, or if you have to use Qt (5.15) from download.qt.io + for any other reason, you should apply the fix to Qt sources: locate + the file (the GCC error message tells exactly where it is), find the line + with the (strange-looking) `ThreadEngineStarter` constructor definition: + ```cplusplus + ThreadEngineStarter(ThreadEngine \*_threadEngine) + ``` + and remove the template specialisation from the constructor name so that it + looks like + ```cplusplus + ThreadEngineStarter(ThreadEngine \*_threadEngine) + ``` + This will fix your build (and any other build involving QtConcurrent from + this installation of Qt - the fix is not specific to Quotient in any way). #### Logging configuration diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index edcdc572..90bc3f9b 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -91,8 +91,9 @@ inline auto connectSingleShot(auto* sender, auto signal, ContextT* context, (context->*slot)(args...); }; # endif - return _impl::connect<_impl::SingleShot>(sender, signal, context, - std::move(boundSlot), connType); + return _impl::connect<_impl::SingleShot>( + sender, signal, context, + std::forward(boundSlot), connType); } else { return _impl::connect<_impl::SingleShot>(sender, signal, context, slot, connType); -- cgit v1.2.3 From 3f586005840e012a98e0f300a726b1cbda75e001 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 7 Jul 2022 10:56:57 +0200 Subject: Adjust Synapse image for tests :latest stopped working for some reason. --- autotests/run-tests.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index 05a215af..e7a228ef 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -1,16 +1,18 @@ mkdir -p data chmod 0777 data +SYNAPSE_IMAGE='matrixdotorg/synapse:v1.61.1' + rm ~/.local/share/testolmaccount -rf docker run -v `pwd`/data:/data --rm \ - -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate + -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no $SYNAPSE_IMAGE generate (cd data && . ../autotests/adjust-config.sh) docker run -d \ --name synapse \ -p 1234:8008 \ -p 8448:8008 \ -p 8008:8008 \ - -v `pwd`/data:/data matrixdotorg/synapse:latest + -v `pwd`/data:/data $SYNAPSE_IMAGE trap "rm -rf ./data/*; docker rm -f synapse 2>&1 >/dev/null; trap - EXIT" EXIT echo Waiting for synapse to start... -- cgit v1.2.3 From 6e4810bfd6794b7fd03803a6de12b8f896cc4654 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 8 Jul 2022 09:29:32 +0200 Subject: Use clang-format 12; update CONTRIBUTING.md wrt code formatting [skip ci] --- .clang-format | 89 +++++++++++++++++++++++++++++++++++++++++---------------- CONTRIBUTING.md | 17 ++++------- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/.clang-format b/.clang-format index 8375204a..f07bad67 100644 --- a/.clang-format +++ b/.clang-format @@ -14,23 +14,26 @@ # to borrow from the WebKit style. The values for such settings try to but # are not guaranteed to coincide with the latest version of the WebKit style. +# This file assumes ClangFormat 12 or newer + --- Language: Cpp BasedOnStyle: WebKit #AccessModifierOffset: -4 AlignAfterOpenBracket: Align -#AlignConsecutiveMacros: false -#AlignConsecutiveAssignments: false -#AlignConsecutiveDeclarations: false +#AlignArrayOfStructures: None # ClangFormat 13 +#AlignConsecutiveMacros: None +#AlignConsecutiveAssignments: None +#AlignConsecutiveDeclarations: None AlignEscapedNewlines: Left -AlignOperands: true # 'Align' since ClangFormat 11 +AlignOperands: Align #AlignTrailingComments: false #AllowAllArgumentsOnNextLine: true -#AllowAllConstructorInitializersOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true #AllowAllParametersOfDeclarationOnNextLine: true #AllowShortEnumsOnASingleLine: true #AllowShortBlocksOnASingleLine: Empty -#AllowShortCaseLabelsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true #AllowShortFunctionsOnASingleLine: All #AllowShortLambdasOnASingleLine: All #AllowShortIfStatementsOnASingleLine: Never @@ -39,41 +42,49 @@ AlignOperands: true # 'Align' since ClangFormat 11 #AlwaysBreakAfterReturnType: None #AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes +#AttributeMacros: +# - __capability #BinPackArguments: true #BinPackParameters: true BraceWrapping: - AfterCaseLabel: false - AfterClass: false - AfterControlStatement: Never # Switch to MultiLine, once https://bugs.llvm.org/show_bug.cgi?id=47936 is fixed - AfterEnum: false +# AfterCaseLabel: false +# AfterClass: false + AfterControlStatement: Never # Switch to MultiLine with ClangFormat 14 (https://bugs.llvm.org/show_bug.cgi?id=47936) +# AfterEnum: false AfterFunction: true - AfterNamespace: false - AfterStruct: false - AfterUnion: false - AfterExternBlock: false - BeforeCatch: false - BeforeElse: false - IndentBraces: false +# AfterNamespace: false +# AfterStruct: false +# AfterUnion: false +# AfterExternBlock: false +# BeforeCatch: false +# BeforeElse: false + BeforeLambdaBody: true +# BeforeWhile: false +# IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBinaryOperators: NonAssignment +#BreakBeforeConceptDeclarations: true BreakBeforeBraces: Custom -#BreakBeforeInheritanceComma: false +#BreakBeforeInheritanceComma: false # deprecated? #BreakInheritanceList: BeforeColon #BreakBeforeTernaryOperators: true #BreakConstructorInitializersBeforeComma: false # deprecated? #BreakConstructorInitializers: BeforeComma #BreakStringLiterals: true ColumnLimit: 80 +#QualifierAlignment: Leave # ClangFormat 14? #CompactNamespaces: false -#ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true #ConstructorInitializerIndentWidth: 4 #ContinuationIndentWidth: 4 -Cpp11BracedListStyle: false +#Cpp11BracedListStyle: true #DeriveLineEnding: true #DerivePointerAlignment: false -FixNamespaceComments: true +#EmptyLineAfterAccessModifier: Never # ClangFormat 14 +EmptyLineBeforeAccessModifier: Always +#FixNamespaceComments: false # See ShortNamespaces below IncludeBlocks: Regroup IncludeCategories: - Regex: '^ Date: Fri, 8 Jul 2022 20:00:28 +0200 Subject: clang-format: don't break lines before lambda body Aside from breaking that line, the previous line - with connect*() - is often broken up too, making smaller lambdas consume much more vertical space. --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index f07bad67..4363a7e4 100644 --- a/.clang-format +++ b/.clang-format @@ -58,7 +58,7 @@ BraceWrapping: # AfterExternBlock: false # BeforeCatch: false # BeforeElse: false - BeforeLambdaBody: true +# BeforeLambdaBody: false # Blows up lambdas vertically, even if they become _very_ readable # BeforeWhile: false # IndentBraces: false SplitEmptyFunction: false -- cgit v1.2.3 From 267219c955b938483c3d113e455c4abd96ef8ce6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 8 Jul 2022 20:00:51 +0200 Subject: qt_connection_util.h: use auto --- lib/qt_connection_util.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 90bc3f9b..ef7f6f80 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -15,8 +15,7 @@ namespace _impl { inline auto connect(auto* sender, auto signal, auto* context, auto slotLike, Qt::ConnectionType connType) { - std::unique_ptr pConn = - std::make_unique(); + auto pConn = std::make_unique(); auto& c = *pConn; // Save the reference before pConn is moved from c = QObject::connect( sender, signal, context, -- cgit v1.2.3 From e2ea64c603b1e4b2184e363ee0d0a13fa0e286a0 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 11 Jul 2022 10:15:09 +0200 Subject: Add QUOTIENT_API to RoomStateView Fixing link errors at non-template RoomStateView::get() when building with libQuotient as a shared object. There's also a test in quotest.cpp now to cover that case. --- lib/roomstateview.h | 3 ++- quotest/quotest.cpp | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/roomstateview.h b/lib/roomstateview.h index cab69ae3..29cce00e 100644 --- a/lib/roomstateview.h +++ b/lib/roomstateview.h @@ -11,7 +11,8 @@ namespace Quotient { class Room; -class RoomStateView : private QHash { +class QUOTIENT_API RoomStateView + : private QHash { Q_GADGET public: const QHash& events() const diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 6bcd71cd..90a5a69b 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -588,6 +588,14 @@ TEST_IMPL(changeName) if (!rme->newDisplayName() || *rme->newDisplayName() != newName) FAIL_TEST(); + // State events coming in the timeline are first + // processed to change the room state and then as + // timeline messages; aboutToAddNewMessages is triggered + // when the state is already updated, so check that + if (targetRoom->currentState().get( + localUser->id()) + != rme) + FAIL_TEST(); clog << "Member rename successful, renaming the account" << endl; const auto newN = newName.mid(0, 5); -- cgit v1.2.3 From 982e2665d141a948a30d9092d588b35875fee7d6 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 10 Jul 2022 20:38:57 +0200 Subject: Small .clang-format adjustments --- .clang-format | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.clang-format b/.clang-format index 4363a7e4..010dc385 100644 --- a/.clang-format +++ b/.clang-format @@ -122,8 +122,8 @@ PenaltyBreakComment: 45 PenaltyBreakString: 200 #PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 40 -PenaltyReturnTypeOnItsOwnLine: 150 -PenaltyIndentedWhitespace: 40 +PenaltyReturnTypeOnItsOwnLine: 200 +#PenaltyIndentedWhitespace: 0 #PointerAlignment: Left #PPIndentWidth: -1 #ReferenceAlignment: Pointer # ClangFormat 13 -- 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(-) 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 2245a44aca8d51e331cf1211ff6e2b38c57e5246 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 8 Jun 2022 14:26:30 +0200 Subject: Pack up StickerEvent in the header file Dropping yet another translation unit. --- CMakeLists.txt | 2 +- lib/events/stickerevent.cpp | 26 -------------------------- lib/events/stickerevent.h | 19 +++++++++++++++---- 3 files changed, 16 insertions(+), 31 deletions(-) delete mode 100644 lib/events/stickerevent.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 94055b7f..b2c4ff83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,7 +165,7 @@ list(APPEND lib_SRCS lib/events/encryptionevent.h lib/events/encryptionevent.cpp lib/events/encryptedevent.h lib/events/encryptedevent.cpp lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp - lib/events/stickerevent.h lib/events/stickerevent.cpp + lib/events/stickerevent.h lib/events/keyverificationevent.h lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp lib/jobs/requestdata.h lib/jobs/requestdata.cpp diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp deleted file mode 100644 index 6d318f0e..00000000 --- a/lib/events/stickerevent.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// SDPX-FileCopyrightText: 2020 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "stickerevent.h" - -using namespace Quotient; - -StickerEvent::StickerEvent(const QJsonObject &obj) - : RoomEvent(typeId(), obj) - , m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject())) -{} - -QString StickerEvent::body() const -{ - return contentPart("body"_ls); -} - -const EventContent::ImageContent &StickerEvent::image() const -{ - return m_imageContent; -} - -QUrl StickerEvent::url() const -{ - return m_imageContent.url(); -} diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h index 0957dca3..e378422d 100644 --- a/lib/events/stickerevent.h +++ b/lib/events/stickerevent.h @@ -16,21 +16,32 @@ class QUOTIENT_API StickerEvent : public RoomEvent public: DEFINE_EVENT_TYPEID("m.sticker", StickerEvent) - explicit StickerEvent(const QJsonObject &obj); + explicit StickerEvent(const QJsonObject& obj) + : RoomEvent(TypeId, obj) + , m_imageContent( + EventContent::ImageContent(obj["content"_ls].toObject())) + {} /// \brief A textual representation or associated description of the /// sticker image. /// /// This could be the alt text of the original image, or a message to /// accompany and further describe the sticker. - QString body() const; + QUO_CONTENT_GETTER(QString, body) /// \brief Metadata about the image referred to in url including a /// thumbnail representation. - const EventContent::ImageContent &image() const; + const EventContent::ImageContent& image() const + { + return m_imageContent; + } /// \brief The URL to the sticker image. This must be a valid mxc:// URI. - QUrl url() const; + QUrl url() const + { + return m_imageContent.url(); + } + private: EventContent::ImageContent m_imageContent; }; -- cgit v1.2.3 From 607489a2e6a3e3238eac0178f5c7bbc70f178f46 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 20:43:50 +0200 Subject: Fix deprecation messages [skip ci] --- lib/events/eventrelation.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h index e445ee42..2a841cf1 100644 --- a/lib/events/eventrelation.h +++ b/lib/events/eventrelation.h @@ -34,11 +34,11 @@ struct QUOTIENT_API EventRelation { return { ReplacementType, std::move(eventId) }; } - [[deprecated("Use ReplyRelation variable instead")]] + [[deprecated("Use ReplyType variable instead")]] static constexpr auto Reply() { return ReplyType; } - [[deprecated("Use AnnotationRelation variable instead")]] // + [[deprecated("Use AnnotationType variable instead")]] // static constexpr auto Annotation() { return AnnotationType; } - [[deprecated("Use ReplacementRelation variable instead")]] // + [[deprecated("Use ReplacementType variable instead")]] // static constexpr auto Replacement() { return ReplacementType; } }; -- cgit v1.2.3 From ab2d78de6a9d33e1470ae9db2ed6ed9565c817e5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 11 Jul 2022 10:08:44 +0200 Subject: fromJson()/toJson() refactoring fromJson() is generalised to accept any JSON-like type while passing QJsonObject to JsonConverter<>::load (instead of doLoad). This allows to (still) rely on JsonConverter<> as a customisation point while providing an opportunity to overload fromJson for custom types in a pointed way (specifically, by providing the overload for `fromJson(const QJsonObject&)`), instead of having to go with full-blown JsonConverter<> specialisation. This will be used in a further commit to simplify ReceiptEvent definition. Using if constexpr in combination with constraints (`requires()`) - the first such case in Quotient codebase - allowed to put the entire logic in a single JsonConverter<>::load() body instead of having a facility JsonExporter<> class for SFINAE. Aside from that, fromJson is entirely dropped because it's not supposed to be used that way (it's no-op after all); reflecting that, Event::unsignedPart() and Event::contentPart() no more default to QJsonValue as the expected return type, you have to explicitly provide the type instead (and as one can see from the changes in the commit, it's actually better that way since it's better to validate the value inside JSON - e.g. check QString or QJsonObject for emptiness - than the QJsonValue envelope which may still wrap an empty value). toJson() is also generalised, replacing 3 functions with one that has a constexpr if to discern between two kinds of types. --- lib/converters.h | 89 ++++++++++++++++++------------------------- lib/events/encryptedevent.cpp | 10 +++-- lib/events/event.h | 4 +- lib/events/roomevent.cpp | 6 +-- lib/events/stateevent.cpp | 2 +- 5 files changed, 50 insertions(+), 61 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index c445442c..9f42d712 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -28,23 +28,8 @@ struct JsonObjectConverter { static void fillFrom(const QJsonObject&, T&) = delete; }; -namespace _impl { - template - struct JsonExporter { - static QJsonObject dump(const T& data) - { - QJsonObject jo; - JsonObjectConverter::dumpTo(jo, data); - return jo; - } - }; - - template - struct JsonExporter< - T, std::enable_if_t>> { - static auto dump(const T& data) { return data.toJson(); } - }; -} +template +PodT fromJson(const JsonT&); //! \brief The switchboard for extra conversion algorithms behind from/toJson //! @@ -62,13 +47,23 @@ namespace _impl { //! that they are not supported and it's not feasible to support those by means //! of overloading toJson() and specialising fromJson(). template -struct JsonConverter : _impl::JsonExporter { +struct JsonConverter { // Unfortunately, if constexpr doesn't work with dump() and T::toJson // because trying to check invocability of T::toJson hits a hard // (non-SFINAE) compilation error if the member is not there. Hence a bit // more verbose SFINAE construct in _impl::JsonExporter. + static auto dump(const T& data) + { + if constexpr (requires() { data.toJson(); }) + return data.toJson(); + else { + QJsonObject jo; + JsonObjectConverter::dumpTo(jo, data); + return jo; + } + } - static T doLoad(const QJsonObject& jo) + static T load(const QJsonObject& jo) { // 'else' below are required to suppress code generation for unused // branches - 'return' is not enough @@ -82,65 +77,57 @@ struct JsonConverter : _impl::JsonExporter { return pod; } } - static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } - static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } + // By default, revert to fromJson() so that one could provide a single + // fromJson specialisation instead of specialising + // the entire JsonConverter; if a different type of JSON value is needed + // (e.g., an array), specialising JsonConverter is inevitable + static T load(QJsonValueRef jvr) { return fromJson(QJsonValue(jvr)); } + static T load(const QJsonValue& jv) { return fromJson(jv.toObject()); } + static T load(const QJsonDocument& jd) { return fromJson(jd.object()); } }; template - requires (!std::is_constructible_v) inline auto toJson(const T& pod) // -> can return anything from which QJsonValue or, in some cases, QJsonDocument // is constructible { - return JsonConverter::dump(pod); + if constexpr (std::is_constructible_v) + return pod; // No-op if QJsonValue can be directly constructed + else + return JsonConverter::dump(pod); } -inline auto toJson(const QJsonObject& jo) { return jo; } -inline auto toJson(const QJsonValue& jv) { return jv; } - template inline void fillJson(QJsonObject& json, const T& data) { JsonObjectConverter::dumpTo(json, data); } -template -inline T fromJson(const QJsonValue& jv) +template +inline PodT fromJson(const JsonT& json) { - return JsonConverter::load(jv); + // JsonT here can be whatever the respective JsonConverter specialisation + // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject + return JsonConverter::load(json); } -template<> -inline QJsonValue fromJson(const QJsonValue& jv) { return jv; } - -template -inline T fromJson(const QJsonDocument& jd) -{ - return JsonConverter::load(jd); -} - -// Convenience fromJson() overloads that deduce T instead of requiring -// the coder to explicitly type it. They still enforce the +// Convenience fromJson() overload that deduces PodT instead of requiring +// the coder to explicitly type it. It still enforces the // overwrite-everything semantics of fromJson(), unlike fillFromJson() -template -inline void fromJson(const QJsonValue& jv, T& pod) -{ - pod = jv.isUndefined() ? T() : fromJson(jv); -} - -template -inline void fromJson(const QJsonDocument& jd, T& pod) +template +inline void fromJson(const JsonT& json, PodT& pod) { - pod = fromJson(jd); + pod = fromJson(json); } template inline void fillFromJson(const QJsonValue& jv, T& pod) { - if (jv.isObject()) + if constexpr (requires() { JsonObjectConverter::fillFrom({}, pod); }) { JsonObjectConverter::fillFrom(jv.toObject(), pod); - else if (!jv.isUndefined()) + return; + } else if (!jv.isUndefined()) pod = fromJson(jv); } diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index c97ccc16..ec00ad4c 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -49,14 +49,16 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const eventObject["event_id"] = id(); eventObject["sender"] = senderId(); eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); - if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) { + if (const auto relatesToJson = contentPart("m.relates_to"_ls); + !relatesToJson.isEmpty()) { auto content = eventObject["content"].toObject(); - content["m.relates_to"] = relatesToJson.toObject(); + content["m.relates_to"] = relatesToJson; eventObject["content"] = content; } - if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) { + if (const auto redactsJson = unsignedPart("redacts"_ls); + !redactsJson.isEmpty()) { auto unsign = eventObject["unsigned"].toObject(); - unsign["redacts"] = redactsJson.toString(); + unsign["redacts"] = redactsJson; eventObject["unsigned"] = unsign; } return loadEvent(eventObject); diff --git a/lib/events/event.h b/lib/events/event.h index 05eb51e9..da6cf3c7 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -212,7 +212,7 @@ public: const QJsonObject contentJson() const; - template + template const T contentPart(KeyT&& key) const { return fromJson(contentJson()[std::forward(key)]); @@ -227,7 +227,7 @@ public: const QJsonObject unsignedJson() const; - template + template const T unsignedPart(KeyT&& key) const { return fromJson(unsignedJson()[std::forward(key)]); diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3ddf5ac4..e695e0ec 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -15,9 +15,9 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { - if (const auto redaction = unsignedPart(RedactedCauseKeyL); - redaction.isObject()) - _redactedBecause = makeEvent(redaction.toObject()); + if (const auto redaction = unsignedPart(RedactedCauseKeyL); + !redaction.isEmpty()) + _redactedBecause = makeEvent(redaction); } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index c343e37f..43dfd6e8 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -21,7 +21,7 @@ StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, bool StateEventBase::repeatsState() const { - const auto prevContentJson = unsignedPart(PrevContentKeyL); + const auto prevContentJson = unsignedPart(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } -- 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/events/encryptionevent.cpp | 50 ++++++++++++++++++++---------------------- lib/events/encryptionevent.h | 35 ++++++++++++----------------- lib/quotient_common.h | 12 +++++++--- lib/room.cpp | 4 ++-- 4 files changed, 49 insertions(+), 52 deletions(-) diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 1654d6f3..8872447b 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -6,47 +6,45 @@ #include "e2ee/e2ee.h" -namespace Quotient { +using namespace Quotient; + static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey }; template <> -struct JsonConverter { - static EncryptionType load(const QJsonValue& jv) - { - const auto& encryptionString = jv.toString(); - for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); - ++it) - if (encryptionString == *it) - return EncryptionType(it - encryptionStrings.begin()); - - if (!encryptionString.isEmpty()) - qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; - return EncryptionType::Undefined; - } -}; -} // namespace Quotient - -using namespace Quotient; +EncryptionType Quotient::fromJson(const QJsonValue& jv) +{ + const auto& encryptionString = jv.toString(); + for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); + ++it) + if (encryptionString == *it) + return EncryptionType(it - encryptionStrings.begin()); + + if (!encryptionString.isEmpty()) + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + return EncryptionType::Undefined; +} EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) - : encryption(fromJson(json[AlgorithmKeyL])) + : encryption(fromJson(json[AlgorithmKeyL])) , algorithm(sanitized(json[AlgorithmKeyL].toString())) - , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000)) - , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) -{} +{ + // NB: fillFromJson only fills the variable if the JSON key exists + fillFromJson(json[RotationPeriodMsKeyL], rotationPeriodMs); + fillFromJson(json[RotationPeriodMsgsKeyL], rotationPeriodMsgs); +} -EncryptionEventContent::EncryptionEventContent(EncryptionType et) +EncryptionEventContent::EncryptionEventContent(Quotient::EncryptionType et) : encryption(et) { - if(encryption != Undefined) { - algorithm = encryptionStrings[encryption]; + if(encryption != Quotient::EncryptionType::Undefined) { + algorithm = encryptionStrings[static_cast(encryption)]; } } QJsonObject EncryptionEventContent::toJson() const { QJsonObject o; - if (encryption != EncryptionType::Undefined) + if (encryption != Quotient::EncryptionType::Undefined) o.insert(AlgorithmKey, algorithm); o.insert(RotationPeriodMsKey, rotationPeriodMs); o.insert(RotationPeriodMsgsKey, rotationPeriodMsgs); diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 945b17e7..91452c3f 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -4,51 +4,44 @@ #pragma once +#include "quotient_common.h" #include "stateevent.h" namespace Quotient { class QUOTIENT_API EncryptionEventContent { public: - enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; - QUO_IMPLICIT EncryptionEventContent(EncryptionType et); - [[deprecated("This constructor will require explicit EncryptionType soon")]] // - explicit EncryptionEventContent() - : EncryptionEventContent(Undefined) - {} + // NOLINTNEXTLINE(google-explicit-constructor) + QUO_IMPLICIT EncryptionEventContent(Quotient::EncryptionType et); explicit EncryptionEventContent(const QJsonObject& json); QJsonObject toJson() const; - EncryptionType encryption; - QString algorithm; - int rotationPeriodMs; - int rotationPeriodMsgs; + Quotient::EncryptionType encryption; + QString algorithm {}; + int rotationPeriodMs = 604'800'000; + int rotationPeriodMsgs = 100; }; -using EncryptionType = EncryptionEventContent::EncryptionType; - class QUOTIENT_API EncryptionEvent : public StateEvent { - Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) - using EncryptionType = EncryptionEventContent::EncryptionType; - Q_ENUM(EncryptionType) + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; explicit EncryptionEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - [[deprecated("This constructor will require an explicit parameter soon")]] // -// explicit EncryptionEvent() -// : EncryptionEvent(QJsonObject()) -// {} explicit EncryptionEvent(EncryptionEventContent&& content) : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) {} - EncryptionType encryption() const { return content().encryption; } - + Quotient::EncryptionType encryption() const { return content().encryption; } QString algorithm() const { return content().algorithm; } int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 233bcaa1..7fec9274 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -107,14 +107,20 @@ enum UriResolveResult : int8_t { }; Q_ENUM_NS(UriResolveResult) -enum RoomType { - Space, - Undefined, +enum class RoomType : uint8_t { + Space = 0, + Undefined = 0xFF, }; Q_ENUM_NS(RoomType) [[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" }; +enum class EncryptionType : uint8_t { + MegolmV1AesSha2 = 0, + Undefined = 0xFF, +}; +Q_ENUM_NS(EncryptionType) + } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) 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 9a65bde0fa438ee4ad101b58721b77182ae1ae70 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 9 May 2022 12:02:30 +0200 Subject: Make AliasesEventContent a simple structure JSON conversions are moved out of the class, obviating the need to define the plain data constructor and gaining default-constructibility along the way - previously the default constructor was preempted by user-defined ones. --- lib/events/roomcanonicalaliasevent.h | 43 ++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index bb8654e5..e599699d 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -7,35 +7,30 @@ #include "stateevent.h" namespace Quotient { -namespace EventContent{ - class AliasesEventContent { - - public: - - template - AliasesEventContent(T1&& canonicalAlias, T2&& altAliases) - : canonicalAlias(std::forward(canonicalAlias)) - , altAliases(std::forward(altAliases)) - { } - - AliasesEventContent(const QJsonObject& json) - : canonicalAlias(fromJson(json["alias"])) - , altAliases(fromJson(json["alt_aliases"])) - { } - - auto toJson() const - { - QJsonObject jo; - addParam(jo, QStringLiteral("alias"), canonicalAlias); - addParam(jo, QStringLiteral("alt_aliases"), altAliases); - return jo; - } - +namespace EventContent { + struct AliasesEventContent { QString canonicalAlias; QStringList altAliases; }; } // namespace EventContent +template<> +inline EventContent::AliasesEventContent fromJson(const QJsonObject& jo) +{ + return EventContent::AliasesEventContent { + fromJson(jo["alias"_ls]), + fromJson(jo["alt_aliases"_ls]) + }; +} +template<> +inline auto toJson(const EventContent::AliasesEventContent& c) +{ + QJsonObject jo; + addParam(jo, QStringLiteral("alias"), c.canonicalAlias); + addParam(jo, QStringLiteral("alt_aliases"), c.altAliases); + return jo; +} + class RoomCanonicalAliasEvent : public StateEvent { public: -- cgit v1.2.3 From e7dee15531ad357bd33ac546fb1b9332a5c1260c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 9 Jun 2022 08:40:27 +0200 Subject: converters.*: facilities to convert enums This introduces enumTo/FromJsonString() and flagTo/FromJsonString(), four facility functions to simplify conversion between C++ enums and JSON, and refactors a couple of places where it's useful. --- lib/converters.cpp | 14 +++++++ lib/converters.h | 92 ++++++++++++++++++++++++++++++++++++++++++ lib/events/roomcreateevent.cpp | 19 +++------ lib/events/roommemberevent.cpp | 19 +++------ 4 files changed, 116 insertions(+), 28 deletions(-) diff --git a/lib/converters.cpp b/lib/converters.cpp index 444ca4f6..b0e3a4b6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -2,9 +2,23 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "converters.h" +#include "logging.h" #include +void Quotient::_impl::warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName) +{ + qWarning(EVENTS).noquote() + << "Unknown" << enumTypeName << "value:" << stringValue; +} + +void Quotient::_impl::reportEnumOutOfBounds(uint32_t v, const char* enumTypeName) +{ + qCritical(MAIN).noquote() + << "Value" << v << "is out of bounds for enumeration" << enumTypeName; +} + QJsonValue Quotient::JsonConverter::dump(const QVariant& v) { return QJsonValue::fromVariant(v); diff --git a/lib/converters.h b/lib/converters.h index 9f42d712..bf456af9 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -131,6 +131,98 @@ inline void fillFromJson(const QJsonValue& jv, T& pod) pod = fromJson(jv); } +namespace _impl { + void warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName); + void reportEnumOutOfBounds(uint32_t v, const char* enumTypeName); +} + +//! \brief Facility string-to-enum converter +//! +//! This is to simplify enum loading from JSON - just specialise +//! Quotient::fromJson() and call this function from it, passing (aside from +//! the JSON value for the enum - that must be a string, not an int) any +//! iterable container of string'y values (const char*, QLatin1String, etc.) +//! matching respective enum values, 0-based. +//! \sa enumToJsonString +template +EnumT enumFromJsonString(const QString& s, const EnumStringValuesT& enumValues, + EnumT defaultValue) +{ + static_assert(std::is_unsigned_v>); + if (const auto it = std::find(cbegin(enumValues), cend(enumValues), s); + it != cend(enumValues)) + return EnumT(it - cbegin(enumValues)); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(EnumT())); + return defaultValue; +} + +//! \brief Facility enum-to-string converter +//! +//! This does the same as enumFromJsonString, the other way around. +//! \note The source enumeration must not have gaps in values, or \p enumValues +//! has to match those gaps (i.e., if the source enumeration is defined +//! as { Value1 = 1, Value2 = 3, Value3 = 5 } then \p enumValues +//! should be defined as { "", "Value1", "", "Value2", "", "Value3" +//! } (mind the gap at value 0, in particular). +//! \sa enumFromJsonString +template +QString enumToJsonString(EnumT v, const EnumStringValuesT& enumValues) +{ + static_assert(std::is_unsigned_v>); + if (v < size(enumValues)) + return enumValues[v]; + + _impl::reportEnumOutOfBounds(static_cast(v), + qt_getEnumName(EnumT())); + Q_ASSERT(false); + return {}; +} + +//! \brief Facility converter for flags +//! +//! This is very similar to enumFromJsonString, except that the target +//! enumeration is assumed to be of a 'flag' kind - i.e. its values must be +//! a power-of-two sequence starting from 1, without gaps, so exactly 1,2,4,8,16 +//! and so on. +//! \note Unlike enumFromJsonString, the values start from 1 and not from 0, +//! with 0 being used for an invalid value by default. +//! \note This function does not support flag combinations. +//! \sa QUO_DECLARE_FLAGS, QUO_DECLARE_FLAGS_NS +template +FlagT flagFromJsonString(const QString& s, const FlagStringValuesT& flagValues, + FlagT defaultValue = FlagT(0U)) +{ + // Enums based on signed integers don't make much sense for flag types + static_assert(std::is_unsigned_v>); + if (const auto it = std::find(cbegin(flagValues), cend(flagValues), s); + it != cend(flagValues)) + return FlagT(1U << (it - cbegin(flagValues))); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(FlagT())); + return defaultValue; +} + +template +QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues) +{ + static_assert(std::is_unsigned_v>); + if (const auto offset = + qCountTrailingZeroBits(std::underlying_type_t(v)); + offset < size(flagValues)) // + { + return flagValues[offset]; + } + + _impl::reportEnumOutOfBounds(static_cast(v), + qt_getEnumName(FlagT())); + Q_ASSERT(false); + return {}; +} + // JsonConverter<> specialisations template<> diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index bb6de648..3b5024d5 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -6,20 +6,11 @@ using namespace Quotient; template <> -struct Quotient::JsonConverter { - static RoomType load(const QJsonValue& jv) - { - const auto& roomTypeString = jv.toString(); - for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end(); - ++it) - if (roomTypeString == *it) - return RoomType(it - RoomTypeStrings.begin()); - - if (!roomTypeString.isEmpty()) - qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString; - return RoomType::Undefined; - } -}; +RoomType Quotient::fromJson(const QJsonValue& jv) +{ + return enumFromJsonString(jv.toString(), RoomTypeStrings, + RoomType::Undefined); +} bool RoomCreateEvent::isFederated() const { diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index c3be0e00..953ff8ae 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -11,18 +11,10 @@ template <> struct JsonConverter { static Membership load(const QJsonValue& jv) { - const auto& ms = jv.toString(); - if (ms.isEmpty()) - { - qCWarning(EVENTS) << "Empty membership state"; - return Membership::Invalid; - } - const auto it = - std::find(MembershipStrings.begin(), MembershipStrings.end(), ms); - if (it != MembershipStrings.end()) - return Membership(1U << (it - MembershipStrings.begin())); - - qCWarning(EVENTS) << "Unknown Membership value: " << ms; + if (const auto& ms = jv.toString(); !ms.isEmpty()) + return flagFromJsonString(ms, MembershipStrings); + + qCWarning(EVENTS) << "Empty membership state"; return Membership::Invalid; } }; @@ -46,8 +38,7 @@ QJsonObject MemberEventContent::toJson() const QJsonObject o; if (membership != Membership::Invalid) o.insert(QStringLiteral("membership"), - MembershipStrings[qCountTrailingZeroBits( - std::underlying_type_t(membership))]); + flagToJsonString(membership, MembershipStrings)); if (displayName) o.insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) -- cgit v1.2.3 From e172699e9a20737c4abb9bd96609f48e65415961 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 22 Jun 2022 13:17:22 +0200 Subject: eventcontent.h: Use C++17 nested namespaces notation --- lib/events/eventcontent.h | 464 +++++++++++++++++++++++----------------------- 1 file changed, 231 insertions(+), 233 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 7611d077..af26c0a4 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -17,240 +17,238 @@ class QFileInfo; -namespace Quotient { -namespace EventContent { - //! \brief Base for all content types that can be stored in RoomMessageEvent +namespace Quotient::EventContent { +//! \brief Base for all content types that can be stored in RoomMessageEvent +//! +//! Each content type class should have a constructor taking +//! a QJsonObject and override fillJson() with an implementation +//! that will fill the target QJsonObject with stored values. It is +//! assumed but not required that a content object can also be created +//! from plain data. +class QUOTIENT_API Base { +public: + explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} + virtual ~Base() = default; + + QJsonObject toJson() const; + +public: + QJsonObject originalJson; + + // You can't assign those classes + Base& operator=(const Base&) = delete; + Base& operator=(Base&&) = delete; + +protected: + Base(const Base&) = default; + Base(Base&&) noexcept = default; + + virtual void fillJson(QJsonObject&) const = 0; +}; + +// The below structures fairly follow CS spec 11.2.1.6. The overall +// set of attributes for each content types is a superset of the spec +// but specific aggregation structure is altered. See doc comments to +// each type for the list of available attributes. + +// A quick classes inheritance structure follows (the definitions are +// spread across eventcontent.h and roommessageevent.h): +// UrlBasedContent : InfoT + thumbnail data +// PlayableContent : + duration attribute +// FileInfo +// FileContent = UrlBasedContent +// AudioContent = PlayableContent +// ImageInfo : FileInfo + imageSize attribute +// ImageContent = UrlBasedContent +// VideoContent = PlayableContent + +//! \brief Mix-in class representing `info` subobject in content JSON +//! +//! This is one of base classes for content types that deal with files or +//! URLs. It stores the file metadata attributes, such as size, MIME type +//! etc. found in the `content/info` subobject of event JSON payloads. +//! Actual content classes derive from this class _and_ TypedBase that +//! provides a polymorphic interface to access data in the mix-in. FileInfo +//! (as well as ImageInfo, that adds image size to the metadata) is NOT +//! polymorphic and is used in a non-polymorphic way to store thumbnail +//! metadata (in a separate instance), next to the metadata on the file +//! itself. +//! +//! If you need to make a new _content_ (not info) class based on files/URLs +//! take UrlBasedContent as the example, i.e.: +//! 1. Double-inherit from this class (or ImageInfo) and TypedBase. +//! 2. Provide a constructor from QJsonObject that will pass the `info` +//! subobject (not the whole content JSON) down to FileInfo/ImageInfo. +//! 3. Override fillJson() to customise the JSON export logic. Make sure +//! to call toInfoJson() from it to produce the payload for the `info` +//! subobject in the JSON payload. +//! +//! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent, +//! UrlBasedContent +class QUOTIENT_API FileInfo { +public: + FileInfo() = default; + //! \brief Construct from a QFileInfo object //! - //! Each content type class should have a constructor taking - //! a QJsonObject and override fillJson() with an implementation - //! that will fill the target QJsonObject with stored values. It is - //! assumed but not required that a content object can also be created - //! from plain data. - class QUOTIENT_API Base { - public: - explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} - virtual ~Base() = default; - - QJsonObject toJson() const; - - public: - QJsonObject originalJson; - - // You can't assign those classes - Base& operator=(const Base&) = delete; - Base& operator=(Base&&) = delete; - - protected: - Base(const Base&) = default; - Base(Base&&) noexcept = default; - - virtual void fillJson(QJsonObject&) const = 0; - }; - - // The below structures fairly follow CS spec 11.2.1.6. The overall - // set of attributes for each content types is a superset of the spec - // but specific aggregation structure is altered. See doc comments to - // each type for the list of available attributes. - - // A quick classes inheritance structure follows (the definitions are - // spread across eventcontent.h and roommessageevent.h): - // UrlBasedContent : InfoT + thumbnail data - // PlayableContent : + duration attribute - // FileInfo - // FileContent = UrlBasedContent - // AudioContent = PlayableContent - // ImageInfo : FileInfo + imageSize attribute - // ImageContent = UrlBasedContent - // VideoContent = PlayableContent - - //! \brief Mix-in class representing `info` subobject in content JSON + //! \param fi a QFileInfo object referring to an existing file + explicit FileInfo(const QFileInfo& fi); + explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, + const QMimeType& mimeType = {}, + QString originalFilename = {}); + //! \brief Construct from a JSON `info` payload //! - //! This is one of base classes for content types that deal with files or - //! URLs. It stores the file metadata attributes, such as size, MIME type - //! etc. found in the `content/info` subobject of event JSON payloads. - //! Actual content classes derive from this class _and_ TypedBase that - //! provides a polymorphic interface to access data in the mix-in. FileInfo - //! (as well as ImageInfo, that adds image size to the metadata) is NOT - //! polymorphic and is used in a non-polymorphic way to store thumbnail - //! metadata (in a separate instance), next to the metadata on the file - //! itself. - //! - //! If you need to make a new _content_ (not info) class based on files/URLs - //! take UrlBasedContent as the example, i.e.: - //! 1. Double-inherit from this class (or ImageInfo) and TypedBase. - //! 2. Provide a constructor from QJsonObject that will pass the `info` - //! subobject (not the whole content JSON) down to FileInfo/ImageInfo. - //! 3. Override fillJson() to customise the JSON export logic. Make sure - //! to call toInfoJson() from it to produce the payload for the `info` - //! subobject in the JSON payload. - //! - //! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent, - //! UrlBasedContent - class QUOTIENT_API FileInfo { - public: - FileInfo() = default; - //! \brief Construct from a QFileInfo object - //! - //! \param fi a QFileInfo object referring to an existing file - explicit FileInfo(const QFileInfo& fi); - explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, - const QMimeType& mimeType = {}, - QString originalFilename = {}); - //! \brief Construct from a JSON `info` payload - //! - //! Make sure to pass the `info` subobject of content JSON, not the - //! whole JSON content. - FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, - QString originalFilename = {}); - - bool isValid() const; - QUrl url() const; - - //! \brief Extract media id from the URL - //! - //! This can be used, e.g., to construct a QML-facing image:// - //! URI as follows: - //! \code "image://provider/" + info.mediaId() \endcode - QString mediaId() const { return url().authority() + url().path(); } - - public: - FileSourceInfo source; - QJsonObject originalInfoJson; - QMimeType mimeType; - qint64 payloadSize = 0; - QString originalName; - }; - - QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); - - //! \brief A content info class for image/video content types and thumbnails - class QUOTIENT_API ImageInfo : public FileInfo { - public: - ImageInfo() = default; - explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); - explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, - const QMimeType& type = {}, QSize imageSize = {}, - const QString& originalFilename = {}); - ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - public: - QSize imageSize; - }; - - QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info); - - //! \brief An auxiliary class for an info type that carries a thumbnail - //! - //! This class saves/loads a thumbnail to/from `info` subobject of - //! the JSON representation of event content; namely, `info/thumbnail_url` - //! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and - //! `info/thumbnail_info` fields are used. - class QUOTIENT_API Thumbnail : public ImageInfo { - public: - using ImageInfo::ImageInfo; - explicit Thumbnail(const QJsonObject& infoJson, - const Omittable& efm = none); - - //! \brief Add thumbnail information to the passed `info` JSON object - void dumpTo(QJsonObject& infoJson) const; - }; - - class QUOTIENT_API TypedBase : public Base { - public: - virtual QMimeType type() const = 0; - virtual const FileInfo* fileInfo() const { return nullptr; } - virtual FileInfo* fileInfo() { return nullptr; } - virtual const Thumbnail* thumbnailInfo() const { return nullptr; } - - protected: - explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} - using Base::Base; - }; - - //! \brief A template class for content types with a URL and additional info - //! - //! Types that derive from this class template take `url` (or, if the file - //! is encrypted, `file`) and, optionally, `filename` values from - //! the top-level JSON object and the rest of information from the `info` - //! subobject, as defined by the parameter type. - //! \tparam InfoT base info class - FileInfo or ImageInfo - template - class UrlBasedContent : public TypedBase, public InfoT { - public: - using InfoT::InfoT; - explicit UrlBasedContent(const QJsonObject& json) - : TypedBase(json) - , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - json["filename"].toString()) - , thumbnail(FileInfo::originalInfoJson) - { - if (const auto efmJson = json.value("file"_ls).toObject(); - !efmJson.isEmpty()) - InfoT::source = fromJson(efmJson); - // Two small hacks on originalJson to expose mediaIds to QML - originalJson.insert("mediaId", InfoT::mediaId()); - originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); - } - - QMimeType type() const override { return InfoT::mimeType; } - const FileInfo* fileInfo() const override { return this; } - FileInfo* fileInfo() override { return this; } - const Thumbnail* thumbnailInfo() const override { return &thumbnail; } - - public: - Thumbnail thumbnail; - - protected: - virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const - {} - - void fillJson(QJsonObject& json) const override - { - Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source); - if (!InfoT::originalName.isEmpty()) - json.insert("filename", InfoT::originalName); - auto infoJson = toInfoJson(*this); - if (thumbnail.isValid()) - thumbnail.dumpTo(infoJson); - fillInfoJson(infoJson); - json.insert("info", infoJson); - } - }; - - //! \brief Content class for m.image - //! - //! Available fields: - //! - corresponding to the top-level JSON: - //! - source (corresponding to `url` or `file` in JSON) - //! - filename (extension to the spec) - //! - corresponding to the `info` subobject: - //! - payloadSize (`size` in JSON) - //! - mimeType (`mimetype` in JSON) - //! - imageSize (QSize for a combination of `h` and `w` in JSON) - //! - thumbnail.url (`thumbnail_url` in JSON) - //! - corresponding to the `info/thumbnail_info` subobject: contents of - //! thumbnail field, in the same vein as for the main image: - //! - payloadSize - //! - mimeType - //! - imageSize - using ImageContent = UrlBasedContent; - - //! \brief Content class for m.file + //! Make sure to pass the `info` subobject of content JSON, not the + //! whole JSON content. + FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + QString originalFilename = {}); + + bool isValid() const; + QUrl url() const; + + //! \brief Extract media id from the URL //! - //! Available fields: - //! - corresponding to the top-level JSON: - //! - source (corresponding to `url` or `file` in JSON) - //! - filename - //! - corresponding to the `info` subobject: - //! - payloadSize (`size` in JSON) - //! - mimeType (`mimetype` in JSON) - //! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) - //! - corresponding to the `info/thumbnail_info` subobject: - //! - thumbnail.payloadSize - //! - thumbnail.mimeType - //! - thumbnail.imageSize (QSize for `h` and `w` in JSON) - using FileContent = UrlBasedContent; -} // namespace EventContent -} // namespace Quotient + //! This can be used, e.g., to construct a QML-facing image:// + //! URI as follows: + //! \code "image://provider/" + info.mediaId() \endcode + QString mediaId() const { return url().authority() + url().path(); } + +public: + FileSourceInfo source; + QJsonObject originalInfoJson; + QMimeType mimeType; + qint64 payloadSize = 0; + QString originalName; +}; + +QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); + +//! \brief A content info class for image/video content types and thumbnails +class QUOTIENT_API ImageInfo : public FileInfo { +public: + ImageInfo() = default; + explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); + explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, + const QMimeType& type = {}, QSize imageSize = {}, + const QString& originalFilename = {}); + ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + const QString& originalFilename = {}); + +public: + QSize imageSize; +}; + +QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info); + +//! \brief An auxiliary class for an info type that carries a thumbnail +//! +//! This class saves/loads a thumbnail to/from `info` subobject of +//! the JSON representation of event content; namely, `info/thumbnail_url` +//! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and +//! `info/thumbnail_info` fields are used. +class QUOTIENT_API Thumbnail : public ImageInfo { +public: + using ImageInfo::ImageInfo; + explicit Thumbnail(const QJsonObject& infoJson, + const Omittable& efm = none); + + //! \brief Add thumbnail information to the passed `info` JSON object + void dumpTo(QJsonObject& infoJson) const; +}; + +class QUOTIENT_API TypedBase : public Base { +public: + virtual QMimeType type() const = 0; + virtual const FileInfo* fileInfo() const { return nullptr; } + virtual FileInfo* fileInfo() { return nullptr; } + virtual const Thumbnail* thumbnailInfo() const { return nullptr; } + +protected: + explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} + using Base::Base; +}; + +//! \brief A template class for content types with a URL and additional info +//! +//! Types that derive from this class template take `url` (or, if the file +//! is encrypted, `file`) and, optionally, `filename` values from +//! the top-level JSON object and the rest of information from the `info` +//! subobject, as defined by the parameter type. +//! \tparam InfoT base info class - FileInfo or ImageInfo +template +class UrlBasedContent : public TypedBase, public InfoT { +public: + using InfoT::InfoT; + explicit UrlBasedContent(const QJsonObject& json) + : TypedBase(json) + , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), + json["filename"].toString()) + , thumbnail(FileInfo::originalInfoJson) + { + if (const auto efmJson = json.value("file"_ls).toObject(); + !efmJson.isEmpty()) + InfoT::source = fromJson(efmJson); + // Two small hacks on originalJson to expose mediaIds to QML + originalJson.insert("mediaId", InfoT::mediaId()); + originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); + } + + QMimeType type() const override { return InfoT::mimeType; } + const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } + const Thumbnail* thumbnailInfo() const override { return &thumbnail; } + +public: + Thumbnail thumbnail; + +protected: + virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const + {} + + void fillJson(QJsonObject& json) const override + { + Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source); + if (!InfoT::originalName.isEmpty()) + json.insert("filename", InfoT::originalName); + auto infoJson = toInfoJson(*this); + if (thumbnail.isValid()) + thumbnail.dumpTo(infoJson); + fillInfoJson(infoJson); + json.insert("info", infoJson); + } +}; + +//! \brief Content class for m.image +//! +//! Available fields: +//! - corresponding to the top-level JSON: +//! - source (corresponding to `url` or `file` in JSON) +//! - filename (extension to the spec) +//! - corresponding to the `info` subobject: +//! - payloadSize (`size` in JSON) +//! - mimeType (`mimetype` in JSON) +//! - imageSize (QSize for a combination of `h` and `w` in JSON) +//! - thumbnail.url (`thumbnail_url` in JSON) +//! - corresponding to the `info/thumbnail_info` subobject: contents of +//! thumbnail field, in the same vein as for the main image: +//! - payloadSize +//! - mimeType +//! - imageSize +using ImageContent = UrlBasedContent; + +//! \brief Content class for m.file +//! +//! Available fields: +//! - corresponding to the top-level JSON: +//! - source (corresponding to `url` or `file` in JSON) +//! - filename +//! - corresponding to the `info` subobject: +//! - payloadSize (`size` in JSON) +//! - mimeType (`mimetype` in JSON) +//! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) +//! - corresponding to the `info/thumbnail_info` subobject: +//! - thumbnail.payloadSize +//! - thumbnail.mimeType +//! - thumbnail.imageSize (QSize for `h` and `w` in JSON) +using FileContent = UrlBasedContent; +} // namespace Quotient::EventContent Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) -- cgit v1.2.3 From 767681c11ec6fecf9d35ba699db31ea2bdcd0702 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 15 Jul 2022 10:33:48 +0200 Subject: Bring back documentation on PROFILE_LOG_USECS I was about to decommission it but got to use it myself. [skip ci] --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d63864d0..fb10c7da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -334,7 +334,10 @@ you might want to setup a `QElapsedTimer` and drop the elapsed time into logs under `PROFILER` logging category. See the existing code for examples - `room.cpp` has quite a few. In order to reduce small timespan logging spam, `PROFILER` log lines are usually guarded by a check that the timer counted big -enough time (200 microseconds by default, 20 microseconds for tighter parts). +enough time (200 microseconds by default, 20 microseconds for tighter parts); +this threshold can be altered at compile-time by defining `PROFILER_LOG_USECS` +preprocessor symbol (i.e. passing `-DPROFILE_LOG_USECS=` to the compiler +if you're on Linux/macOS). ### Generated C++ code for CS API The code in `lib/csapi`, `lib/identity` and `lib/application-service`, although -- cgit v1.2.3 From 24a206f6429f6fa29b9aa1ce39b7e4694e39b046 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 15 Jul 2022 10:32:27 +0200 Subject: operator<<(QDebug, QElapsedTimer): always use ms That switch between micro- and milliseconds was pure visual sugaring, in a potentially time-sensitive context. Also: there's no sense in using const-ref for a small parameter in a function that is, to top it off, almost always inlined. --- lib/logging.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/logging.h b/lib/logging.h index c8d17210..2599efbf 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -72,12 +72,9 @@ inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm) return qdm(debug_object); } -inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) +inline QDebug operator<<(QDebug debug_object, QElapsedTimer et) { - auto val = et.nsecsElapsed() / 1000; - if (val < 1000) - debug_object << val << "µs"; - else - debug_object << val / 1000 << "ms"; + // Keep 3 decimal digits (the first division is int, the second is float) + debug_object << et.nsecsElapsed() / 1000 / 1000.0 << "ms"; return debug_object; } -- cgit v1.2.3 From 78c1a2db7c42b7d2cd5a036a5ef19bb95c679b86 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 15 Jul 2022 14:31:01 +0200 Subject: Connection::user(): validate after lookup, not before If userMap only holds valid ids, there's no reason to spend time validating the sought id: if it's invalid, it won't be found. And lookups over a hash map are cheap. --- lib/connection.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 701f78c2..81151135 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1456,12 +1456,14 @@ User* Connection::user(const QString& uId) { if (uId.isEmpty()) return nullptr; + if (const auto v = d->userMap.value(uId, nullptr)) + return v; + // Before creating a user object, check that the user id is well-formed + // (it's faster to just do a lookup above before validation) if (!uId.startsWith('@') || serverPart(uId).isEmpty()) { qCCritical(MAIN) << "Malformed userId:" << uId; return nullptr; } - if (d->userMap.contains(uId)) - return d->userMap.value(uId); auto* user = userFactory()(this, uId); d->userMap.insert(uId, user); emit newUser(user); -- cgit v1.2.3 From 2cc19e66c0bed3065cf7274dbc908bcb90b7502e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 16 Jul 2022 20:30:56 +0200 Subject: logging.h: suppress clang-tidy warnings --- lib/logging.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/logging.h b/lib/logging.h index 2599efbf..1fafa04b 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -69,12 +69,13 @@ inline qint64 profilerMinNsecs() */ inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm) { - return qdm(debug_object); + return qdm(debug_object); // NOLINT(performance-unnecessary-value-param) } inline QDebug operator<<(QDebug debug_object, QElapsedTimer et) { - // Keep 3 decimal digits (the first division is int, the second is float) - debug_object << et.nsecsElapsed() / 1000 / 1000.0 << "ms"; + // NOLINTNEXTLINE(bugprone-integer-division) + debug_object << static_cast(et.nsecsElapsed() / 1000) / 1000 + << "ms"; // Show in ms with 3 decimal digits precision return debug_object; } -- 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(-) 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 ++++++++++++++++++++++++++++++++++------------------------- lib/room.h | 4 +- 2 files changed, 111 insertions(+), 83 deletions(-) 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; } diff --git a/lib/room.h b/lib/room.h index 0636c4bb..44504691 100644 --- a/lib/room.h +++ b/lib/room.h @@ -973,9 +973,9 @@ Q_SIGNALS: void displayedChanged(bool displayed); void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); - //! The event that m.read receipt points to has changed + //! The event the m.read receipt points to has changed for the listed users //! \sa lastReadReceipt - void lastReadEventChanged(Quotient::User* user); + void lastReadEventChanged(QVector userIds); void fullyReadMarkerMoved(QString fromEventId, QString toEventId); //! \deprecated since 0.7 - use fullyReadMarkerMoved void readMarkerMoved(QString fromEventId, QString toEventId); -- 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(-) 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(-) 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 a329538c617bad01ffd35a83a0b684f90993dd0d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 26 Jul 2022 14:26:05 +0200 Subject: Add missing QUOTIENT_API pieces The upcoming event type infrastructure finally helps to detect those omissions more or less reliably (for event types only though). --- lib/events/redactionevent.h | 2 +- lib/events/roomcanonicalaliasevent.h | 2 +- lib/events/simplestateevents.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index 1c486a44..63617e54 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -6,7 +6,7 @@ #include "roomevent.h" namespace Quotient { -class RedactionEvent : public RoomEvent { +class QUOTIENT_API RedactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index e599699d..60ca68ac 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -31,7 +31,7 @@ inline auto toJson(const EventContent::AliasesEventContent& c) return jo; } -class RoomCanonicalAliasEvent +class QUOTIENT_API RoomCanonicalAliasEvent : public StateEvent { public: DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 33221542..43c1da69 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -34,7 +34,7 @@ DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) constexpr auto RoomAliasesEventKey = "aliases"_ls; -class [[deprecated( +class QUOTIENT_API [[deprecated( "m.room.aliases events are deprecated by the Matrix spec; use" " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // RoomAliasesEvent -- cgit v1.2.3 From d422b1fd87c48572ae1fc162ce239196e3254752 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 26 Jul 2022 16:56:41 +0200 Subject: Hopefully fix building with GCC The last commit broke it. --- lib/events/simplestateevents.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 43c1da69..c6b91931 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -34,10 +34,10 @@ DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) constexpr auto RoomAliasesEventKey = "aliases"_ls; -class QUOTIENT_API [[deprecated( +class [[deprecated( "m.room.aliases events are deprecated by the Matrix spec; use" " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // -RoomAliasesEvent +QUOTIENT_API RoomAliasesEvent : public StateEvent< EventContent::SingleKeyValue> { -- cgit v1.2.3 From 7754947d7f758ddcb31d4ff3ea79435fb1c171e9 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 26 Jul 2022 17:18:36 +0200 Subject: Another fix attempt --- lib/events/simplestateevents.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index c6b91931..46e1b3a7 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -34,13 +34,12 @@ DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) constexpr auto RoomAliasesEventKey = "aliases"_ls; -class [[deprecated( +class Q_DECL_DEPRECATED_X( "m.room.aliases events are deprecated by the Matrix spec; use" - " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // -QUOTIENT_API RoomAliasesEvent + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") + QUOTIENT_API RoomAliasesEvent : public StateEvent< - EventContent::SingleKeyValue> -{ + EventContent::SingleKeyValue> { public: DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) explicit RoomAliasesEvent(const QJsonObject& obj) -- cgit v1.2.3 From 36344a6e0283f924b72cb2b25001bdf212a7e707 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 26 Jul 2022 23:06:50 +0200 Subject: ...and the definitive fix --- lib/events/simplestateevents.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 46e1b3a7..a8eaab56 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -34,10 +34,7 @@ DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) constexpr auto RoomAliasesEventKey = "aliases"_ls; -class Q_DECL_DEPRECATED_X( - "m.room.aliases events are deprecated by the Matrix spec; use" - " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") - QUOTIENT_API RoomAliasesEvent +class QUOTIENT_API RoomAliasesEvent : public StateEvent< EventContent::SingleKeyValue> { public: @@ -45,7 +42,13 @@ public: explicit RoomAliasesEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} + Q_DECL_DEPRECATED_X( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") QString server() const { return stateKey(); } + Q_DECL_DEPRECATED_X( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") QStringList aliases() const { return content().value; } }; } // namespace Quotient -- cgit v1.2.3 From 953d5a9d03b2a3ca439a79775a0c212965d91c20 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 29 Jul 2022 17:50:17 +0200 Subject: Moving eventCast() In a situation where you have an EventPtr that you want to place somewhere as an `event_ptr_tt` you have to carefully check that the stored event is actually of SomeMoreSpecificType and if it is, release() that event pointer, downcast, and re-wrap it into that new event_ptr_tt - or, as can be seen from the diff here, re-loadEvent() from JSON, which is simpler but inefficient. To help clients, and the library, eventCast() can now accept an rvalue smart pointer and do all the necessary things with it. --- lib/connection.cpp | 30 +++++++++++++++--------------- lib/events/event.h | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 81151135..6e04883e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -977,22 +977,22 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) if (!toDeviceEvents.empty()) { qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; - visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { - if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Unsupported algorithm" << event.id() - << "for event" << event.algorithm(); - return; - } - if (isKnownCurveKey(event.senderId(), event.senderKey())) { - handleEncryptedToDeviceEvent(event); - return; + for (auto&& tdEvt : toDeviceEvents) + if (auto&& event = eventCast(std::move(tdEvt))) { + if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Unsupported algorithm" << event->id() + << "for event" << event->algorithm(); + return; + } + if (isKnownCurveKey(event->senderId(), event->senderKey())) { + handleEncryptedToDeviceEvent(*event); + return; + } + trackedUsers += event->senderId(); + outdatedUsers += event->senderId(); + encryptionUpdateRequired = true; + pendingEncryptedEvents.push_back(std::move(event)); } - trackedUsers += event.senderId(); - outdatedUsers += event.senderId(); - encryptionUpdateRequired = true; - pendingEncryptedEvents.push_back( - makeEvent(event.fullJson())); - }); } #endif } diff --git a/lib/events/event.h b/lib/events/event.h index da6cf3c7..043d4f54 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -332,6 +332,13 @@ inline bool isUnknown(const Event& e) return e.type() == UnknownEventTypeId; } +//! \brief Cast the event pointer down in a type-safe way +//! +//! Checks that the event \p eptr points to actually is of the requested type +//! and returns a (plain) pointer to the event downcast to that type. \p eptr +//! can be either "dumb" (BaseEventT*) or "smart" (`event_ptr_tt<>`). This +//! overload doesn't affect the event ownership - if the original pointer owns +//! the event it must outlive the downcast pointer to keep it from dangling. template inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast(&*eptr)) @@ -341,6 +348,28 @@ inline auto eventCast(const BasePtrT& eptr) : nullptr; } +//! \brief Cast the event pointer down in a type-safe way, with moving +//! +//! Checks that the event \p eptr points to actually is of the requested type; +//! if (and only if) it is, releases the pointer, downcasts it to the requested +//! event type and returns a new smart pointer wrapping the downcast one. +//! Unlike the non-moving eventCast() overload, this one only accepts a smart +//! pointer, and that smart pointer should be an rvalue (either a temporary, +//! or as a result of std::move()). The ownership, respectively, is transferred +//! to the new pointer; the original smart pointer is reset to nullptr, as is +//! normal for `unique_ptr<>::release()`. +//! \note If \p eptr's event type does not match \p EventT it retains ownership +//! after calling this overload; if it is a temporary, this normally +//! leads to the event getting deleted along with the end of +//! the temporary's lifetime. +template +inline auto eventCast(event_ptr_tt&& eptr) +{ + return eptr && is>(*eptr) + ? event_ptr_tt(static_cast(eptr.release())) + : nullptr; +} + namespace _impl { template concept Invocable_With_Downcast = -- cgit v1.2.3 From d4b8f54f764bec5758c8f672d4ab05d59e02c269 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 30 Jul 2022 07:37:15 +0200 Subject: moving eventCast(): disallow passing nullptr This is aligned with the non-moving version. --- lib/events/event.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index 043d4f54..b7454337 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -343,9 +343,9 @@ template inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast(&*eptr)) { - Q_ASSERT(eptr); - return is>(*eptr) ? static_cast(&*eptr) - : nullptr; + return eptr && is>(*eptr) + ? static_cast(&*eptr) + : nullptr; } //! \brief Cast the event pointer down in a type-safe way, with moving -- cgit v1.2.3 From 150df8ce08976ac7b8c25dbed1b965b7c2f65249 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 1 Aug 2022 12:11:58 +0200 Subject: Pull out common JsonConverter code to JsonObjectUnpacker --- lib/converters.h | 20 ++++++++++++-------- lib/events/eventloader.h | 11 ++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index bf456af9..c1d1e9f9 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -31,6 +31,17 @@ struct JsonObjectConverter { template PodT fromJson(const JsonT&); +template +struct JsonObjectUnpacker { + // By default, revert to fromJson() so that one could provide a single + // fromJson specialisation instead of specialising + // the entire JsonConverter; if a different type of JSON value is needed + // (e.g., an array), specialising JsonConverter is inevitable + static T load(QJsonValueRef jvr) { return fromJson(QJsonValue(jvr)); } + static T load(const QJsonValue& jv) { return fromJson(jv.toObject()); } + static T load(const QJsonDocument& jd) { return fromJson(jd.object()); } +}; + //! \brief The switchboard for extra conversion algorithms behind from/toJson //! //! This template is mainly intended for partial conversion specialisations @@ -47,7 +58,7 @@ PodT fromJson(const JsonT&); //! that they are not supported and it's not feasible to support those by means //! of overloading toJson() and specialising fromJson(). template -struct JsonConverter { +struct JsonConverter : JsonObjectUnpacker { // Unfortunately, if constexpr doesn't work with dump() and T::toJson // because trying to check invocability of T::toJson hits a hard // (non-SFINAE) compilation error if the member is not there. Hence a bit @@ -77,13 +88,6 @@ struct JsonConverter { return pod; } } - // By default, revert to fromJson() so that one could provide a single - // fromJson specialisation instead of specialising - // the entire JsonConverter; if a different type of JSON value is needed - // (e.g., an array), specialising JsonConverter is inevitable - static T load(QJsonValueRef jvr) { return fromJson(QJsonValue(jvr)); } - static T load(const QJsonValue& jv) { return fromJson(jv.toObject()); } - static T load(const QJsonDocument& jd) { return fromJson(jd.object()); } }; template diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index c7b82e8e..7dde9786 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -48,14 +48,11 @@ inline StateEventPtr loadStateEvent(const QString& matrixType, } template -struct JsonConverter> { - static auto load(const QJsonValue& jv) +struct JsonConverter> + : JsonObjectUnpacker> { + static auto load(const QJsonObject& jo) { - return loadEvent(jv.toObject()); - } - static auto load(const QJsonDocument& jd) - { - return loadEvent(jd.object()); + return loadEvent(jo); } }; } // namespace Quotient -- cgit v1.2.3 From 88dcdd3ef0af4030a963b08bf8c5bff0784c320d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 1 Aug 2022 13:51:58 +0200 Subject: Fix FTBFS --- lib/converters.h | 1 + lib/events/eventloader.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/converters.h b/lib/converters.h index c1d1e9f9..688f7bbd 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -74,6 +74,7 @@ struct JsonConverter : JsonObjectUnpacker { } } + using JsonObjectUnpacker::load; static T load(const QJsonObject& jo) { // 'else' below are required to suppress code generation for unused diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 7dde9786..15271ab1 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -50,9 +50,11 @@ inline StateEventPtr loadStateEvent(const QString& matrixType, template struct JsonConverter> : JsonObjectUnpacker> { + using JsonObjectUnpacker>::load; static auto load(const QJsonObject& jo) { return loadEvent(jo); } }; + } // namespace Quotient -- cgit v1.2.3 From dd7487070b6e1e8cf1097f8c78579ab19cf4892e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 1 Aug 2022 14:03:34 +0200 Subject: EventItem: use doc-comments correctly [skip ci] --- lib/eventitem.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/eventitem.h b/lib/eventitem.h index e476c66c..90d9f458 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -22,16 +22,16 @@ namespace EventStatus { * All values except Redacted and Hidden are mutually exclusive. */ enum Code { - Normal = 0x0, //< No special designation - Submitted = 0x01, //< The event has just been submitted for sending - FileUploaded = 0x02, //< The file attached to the event has been - // uploaded to the server - Departed = 0x03, //< The event has left the client - ReachedServer = 0x04, //< The server has received the event - SendingFailed = 0x05, //< The server could not receive the event - Redacted = 0x08, //< The event has been redacted - Replaced = 0x10, //< The event has been replaced - Hidden = 0x100, //< The event should not be shown in the timeline + Normal = 0x0, ///< No special designation + Submitted = 0x01, ///< The event has just been submitted for sending + FileUploaded = 0x02, ///< The file attached to the event has been + /// uploaded to the server + Departed = 0x03, ///< The event has left the client + ReachedServer = 0x04, ///< The server has received the event + SendingFailed = 0x05, ///< The server could not receive the event + Redacted = 0x08, ///< The event has been redacted + Replaced = 0x10, ///< The event has been replaced + Hidden = 0x100, ///< The event should not be shown in the timeline }; Q_ENUM_NS(Code) } // namespace EventStatus -- cgit v1.2.3 From 16fbc40f741594f44c84b29842f9287c7e2e0d5b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 4 Aug 2022 15:47:13 +0200 Subject: .clang-format: fewer empty lines before access blocks in classes [skip ci] --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 010dc385..70864160 100644 --- a/.clang-format +++ b/.clang-format @@ -83,7 +83,7 @@ ConstructorInitializerAllOnOneLineOrOnePerLine: true #DeriveLineEnding: true #DerivePointerAlignment: false #EmptyLineAfterAccessModifier: Never # ClangFormat 14 -EmptyLineBeforeAccessModifier: Always +EmptyLineBeforeAccessModifier: LogicalBlock #FixNamespaceComments: false # See ShortNamespaces below IncludeBlocks: Regroup IncludeCategories: -- cgit v1.2.3 From deb6c1141af445de6f3f1fd5afc83ed2ac4def4b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 27 Jul 2022 20:10:03 +0200 Subject: Fix Connection::accountData<>() The template version has never worked, to the point where instantiating it would immediately lead to FTBFS. The new version returns an event pointer as a simpler fix that would make it usable - in particular, there's no more need to have separate Connection::Private::unpackAccountData(). To simplify the fix, eventCast() has been made more tolerating - passing nullptr to it is processed in an expected (no-op) way now. --- lib/connection.cpp | 11 +---------- lib/connection.h | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 6e04883e..9e4444df 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -175,15 +175,6 @@ public: void consumeToDeviceEvents(Events&& toDeviceEvents); void consumeDevicesList(DevicesList&& devicesList); - template - EventT* unpackAccountData() const - { - const auto& eventIt = accountData.find(EventT::matrixTypeId()); - return eventIt == accountData.end() - ? nullptr - : weakPtrCast(eventIt->second); - } - void packAndSendAccountData(EventPtr&& event) { const auto eventType = event->matrixType(); @@ -1665,7 +1656,7 @@ bool Connection::isIgnored(const User* user) const IgnoredUsersList Connection::ignoredUsers() const { - const auto* event = d->unpackAccountData(); + const auto* event = accountData(); return event ? event->ignored_users() : IgnoredUsersList(); } diff --git a/lib/connection.h b/lib/connection.h index b8246ecb..0d0c85b6 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -174,24 +174,25 @@ public: */ bool hasAccountData(const QString& type) const; - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ + //! \brief Get a generic account data event of the given type + //! + //! \return an account data event of the given type stored on the server, + //! or nullptr if there's none of that type. + //! \note Direct chats map cannot be retrieved using this method _yet_; + //! use directChats() instead. const EventPtr& accountData(const QString& type) const; - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ + //! \brief Get an account data event of the given type + //! + //! \return the account data content for the given event type stored + //! on the server, or a default-constructed object if there's none + //! of that type. + //! \note Direct chats map cannot be retrieved using this method _yet_; + //! use directChats() instead. template - const typename EventT::content_type accountData() const + const EventT* accountData() const { - if (const auto& eventPtr = accountData(EventT::matrixTypeId())) - return eventPtr->content(); - return {}; + return eventCast(accountData(EventT::TypeId)); } /** Get account data as a JSON object -- 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/events/eventloader.h | 35 ++++++++++------------------------- lib/events/stateevent.cpp | 2 +- lib/events/stateevent.h | 8 ++++---- lib/room.cpp | 5 +++-- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 15271ab1..4c639efa 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -19,32 +19,17 @@ inline event_ptr_tt loadEvent(const QJsonObject& fullJson) return doLoadEvent(fullJson, fullJson[TypeKeyL].toString()); } -/*! Create an event from a type string and content JSON - * - * Use this factory template to resolve the C++ type from the Matrix - * type string in \p matrixType and create an event of that type that has - * its content part set to \p content. - */ -template -inline event_ptr_tt loadEvent(const QString& matrixType, - const QJsonObject& content) -{ - return doLoadEvent(Event::basicJson(matrixType, content), - matrixType); -} - -/*! Create a state event from a type string, content JSON and state key - * - * Use this factory to resolve the C++ type from the Matrix type string - * in \p matrixType and create a state event of that type with content part - * set to \p content and state key set to \p stateKey (empty by default). - */ -inline StateEventPtr loadStateEvent(const QString& matrixType, - const QJsonObject& content, - const QString& stateKey = {}) +//! \brief Create an event from a type string and content JSON +//! +//! Use this template to resolve the C++ type from the Matrix type string in +//! \p matrixType and create an event of that type by passing all parameters +//! to BaseEventT::basicJson(). +template +inline event_ptr_tt loadEvent( + const QString& matrixType, const BasicJsonParamTs&... basicJsonParams) { - return doLoadEvent( - StateEventBase::basicJson(matrixType, content, stateKey), matrixType); + return doLoadEvent( + BaseEventT::basicJson(matrixType, basicJsonParams...), matrixType); } template diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 43dfd6e8..1df24df0 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -16,7 +16,7 @@ StateEventBase::StateEventBase(Type type, const QJsonObject& json) StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) - : RoomEvent(type, basicJson(matrixType, contentJson, stateKey)) + : RoomEvent(type, basicJson(type, stateKey, contentJson)) {} bool StateEventBase::repeatsState() const diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 343e87a5..9f1d7118 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -19,12 +19,12 @@ public: //! Make a minimal correct Matrix state event JSON static QJsonObject basicJson(const QString& matrixTypeId, - const QJsonObject& content, - const QString& stateKey = {}) + const QString& stateKey = {}, + const QJsonObject& contentJson = {}) { return { { TypeKey, matrixTypeId }, { StateKeyKey, stateKey }, - { ContentKey, content } }; + { ContentKey, contentJson } }; } bool isStateEvent() const override { return true; } @@ -41,7 +41,7 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, const QJsonObject& content, const QString& stateKey = {}) { - return StateEventBase::basicJson(matrixTypeId, content, stateKey); + return StateEventBase::basicJson(matrixTypeId, stateKey, content); } //! \brief Override RoomEvent factory with that from StateEventBase if JSON has 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(+) 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 1c94d1b41eb352b31b2dc915fea95e26f6138284 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 19:28:10 +0200 Subject: KeyVerificationSession: cleanup - Use std::chrono for the timeout (it's more readable and less ambiguous) and make it a local variable - Only pass a Connection object once to constructors - Ensure buildability even without E2EE (key verification is disabled in that case) - Reorder #includes - Other cleanup following clang-tidy warnings --- CMakeLists.txt | 4 +-- lib/connection.cpp | 26 +++++++++--------- lib/connection.h | 21 ++++++++------- lib/database.cpp | 1 - lib/keyverificationsession.cpp | 59 +++++++++++++++++++++-------------------- lib/keyverificationsession.h | 60 ++++++++++++++++++++++-------------------- 6 files changed, 90 insertions(+), 81 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aebe070..f8667645 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,7 +139,6 @@ list(APPEND lib_SRCS lib/eventitem.h lib/eventitem.cpp lib/accountregistry.h lib/accountregistry.cpp lib/mxcreply.h lib/mxcreply.cpp - lib/keyverificationsession.h lib/keyverificationsession.cpp lib/events/event.h lib/events/event.cpp lib/events/eventloader.h lib/events/roomevent.h lib/events/roomevent.cpp @@ -168,7 +167,6 @@ list(APPEND lib_SRCS lib/events/encryptedevent.h lib/events/encryptedevent.cpp lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp lib/events/stickerevent.h - lib/events/keyverificationevent.h lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp lib/jobs/requestdata.h lib/jobs/requestdata.cpp lib/jobs/basejob.h lib/jobs/basejob.cpp @@ -180,6 +178,7 @@ list(APPEND lib_SRCS if (${PROJECT_NAME}_ENABLE_E2EE) list(APPEND lib_SRCS lib/database.h lib/database.cpp + lib/keyverificationsession.h lib/keyverificationsession.cpp lib/e2ee/qolmaccount.h lib/e2ee/qolmaccount.cpp lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp lib/e2ee/qolminboundsession.h lib/e2ee/qolminboundsession.cpp @@ -189,6 +188,7 @@ if (${PROJECT_NAME}_ENABLE_E2EE) lib/e2ee/qolmerrors.h lib/e2ee/qolmerrors.cpp lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp lib/e2ee/qolmmessage.h lib/e2ee/qolmmessage.cpp + lib/events/keyverificationevent.h ) endif() diff --git a/lib/connection.cpp b/lib/connection.cpp index 3e1e556f..fbe365de 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -37,15 +37,17 @@ #ifdef Quotient_E2EE_ENABLED # include "database.h" +# include "keyverificationsession.h" + # include "e2ee/qolmaccount.h" # include "e2ee/qolminboundsession.h" # include "e2ee/qolmsession.h" # include "e2ee/qolmutility.h" # include "e2ee/qolmutils.h" -# include "events/keyverificationevent.h" -# include "keyverificationsession.h" +# include "events/keyverificationevent.h" #endif // Quotient_E2EE_ENABLED + #if QT_VERSION_MAJOR >= 6 # include #else @@ -988,7 +990,8 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) } switchOnType(*tdEvt, [this](const KeyVerificationRequestEvent& event) { - auto session = new KeyVerificationSession(q->userId(), event, q, false, q); + auto session = new KeyVerificationSession(q->userId(), + event, q, false); emit q->newKeyVerificationSession(session); }, [this](const KeyVerificationReadyEvent& event) { emit q->incomingKeyVerificationReady(event); @@ -1028,8 +1031,8 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve << "is not found at the connection" << q->objectName(); } }, [this](const KeyVerificationRequestEvent& event) { - auto session = new KeyVerificationSession(q->userId(), event, q, true, q); - emit q->newKeyVerificationSession(session); + emit q->newKeyVerificationSession( + new KeyVerificationSession(q->userId(), event, q, true)); }, [this](const KeyVerificationReadyEvent& event) { emit q->incomingKeyVerificationReady(event); }, [this](const KeyVerificationStartEvent& event) { @@ -2274,9 +2277,9 @@ QString Connection::Private::curveKeyForUserDevice(const QString& userId, } QString Connection::edKeyForUserDevice(const QString& userId, - const QString& device) const + const QString& deviceId) const { - return d->deviceKeys[userId][device].keys["ed25519:" % device]; + return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId]; } bool Connection::Private::isKnownCurveKey(const QString& userId, @@ -2458,12 +2461,10 @@ void Connection::saveCurrentOutboundMegolmSession( session); } -#endif - void Connection::startKeyVerificationSession(const QString& deviceId) { - auto session = new KeyVerificationSession(userId(), deviceId, this, this); - Q_EMIT newKeyVerificationSession(session); + auto* const session = new KeyVerificationSession(userId(), deviceId, this); + emit newKeyVerificationSession(session); } void Connection::sendToDevice(const QString& userId, const QString& deviceId, @@ -2499,7 +2500,7 @@ void Connection::sendToDevice(const QString& userId, const QString& deviceId, { { userId, { { deviceId, event->contentJson() } } } }); } -bool Connection::isVerifiedSession(const QString& megolmSessionId) +bool Connection::isVerifiedSession(const QString& megolmSessionId) const { auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls); query.bindValue(":sessionId", megolmSessionId); @@ -2520,3 +2521,4 @@ bool Connection::isVerifiedSession(const QString& megolmSessionId) database()->execute(query); return query.next() && query.value("verified").toBool(); } +#endif diff --git a/lib/connection.h b/lib/connection.h index b684d16b..3a4ee798 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -24,10 +24,9 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" -#include "e2ee/qolmmessage.h" #include "e2ee/qolmoutboundsession.h" -#include "events/keyverificationevent.h" #include "keyverificationsession.h" +#include "events/keyverificationevent.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -332,12 +331,16 @@ public: void saveCurrentOutboundMegolmSession( const QString& roomId, const QOlmOutboundGroupSession& session) const; - - QString edKeyForUserDevice(const QString& user, const QString& device) const; + QString edKeyForUserDevice(const QString& userId, + const QString& deviceId) const; bool hasOlmSession(const QString& user, const QString& deviceId) const; + // This assumes that an olm session already exists. If it doesn't, no message is sent. + void sendToDevice(const QString& userId, const QString& deviceId, + event_ptr_tt event, bool encrypted); + /// Returns true if this megolm session comes from a verified device - bool isVerifiedSession(const QString& megolmSessionId); + bool isVerifiedSession(const QString& megolmSessionId) const; void sendSessionKeyToDevices(const QString& roomId, const QByteArray& sessionId, @@ -528,9 +531,6 @@ public: /// Saves the olm account data to disk. Usually doesn't need to be called manually. void saveOlmAccount(); - // This assumes that an olm session already exists. If it doesn't, no message is sent. - void sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt event, bool encrypted); - public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// @@ -708,9 +708,9 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); +#ifdef Quotient_E2EE_ENABLED void startKeyVerificationSession(const QString& deviceId); -#ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); #endif @@ -871,6 +871,8 @@ Q_SIGNALS: void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); void devicesListLoaded(); + +#ifdef Quotient_E2EE_ENABLED void incomingKeyVerificationReady(const KeyVerificationReadyEvent& event); void incomingKeyVerificationStart(const KeyVerificationStartEvent& event); void incomingKeyVerificationAccept(const KeyVerificationAcceptEvent& event); @@ -881,6 +883,7 @@ Q_SIGNALS: void newKeyVerificationSession(KeyVerificationSession* session); void sessionVerified(const QString& userId, const QString& deviceId); +#endif protected: /** diff --git a/lib/database.cpp b/lib/database.cpp index 79793b9d..4eb82cf5 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index d889b465..1ee489ea 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -1,53 +1,59 @@ // SPDX-FileCopyrightText: 2022 Tobias Fella // SPDX-License-Identifier: LGPL-2.1-or-later -#include "connection.h" #include "keyverificationsession.h" -#include "olm/sas.h" + +#include "connection.h" +#include "database.h" #include "e2ee/qolmaccount.h" #include "e2ee/qolmutils.h" +#include "olm/sas.h" + #include "events/event.h" + #include -#include #include -#include "database.h" +#include + +#include using namespace Quotient; +using namespace std::chrono; -KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection *connection, bool encrypted, QObject* parent) - : QObject(parent) - , m_remoteUserId(remoteUserId) +KeyVerificationSession::KeyVerificationSession( + QString remoteUserId, const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted) + : QObject(connection) + , m_remoteUserId(std::move(remoteUserId)) , m_remoteDeviceId(event.fromDevice()) , m_transactionId(event.transactionId()) , m_connection(connection) , m_encrypted(encrypted) , m_remoteSupportedMethods(event.methods()) { - auto timeoutTime = std::min(event.timestamp().addSecs(600), - QDateTime::currentDateTime().addSecs(120)); - m_timeout = - timeoutTime.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(); - if (m_timeout <= 5000) { - return; - } - init(); - setState(INCOMING); + const auto& currentTime = QDateTime::currentDateTime(); + const auto timeoutTime = + std::min(event.timestamp().addSecs(600), currentTime.addSecs(120)); + const milliseconds timeout{ currentTime.msecsTo(timeoutTime) }; + if (timeout > 5s) + init(timeout); + // Otherwise don't even bother starting up } -KeyVerificationSession::KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent) - : QObject(parent) - , m_remoteUserId(userId) - , m_remoteDeviceId(deviceId) +KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, + Connection* connection) + : QObject(connection) + , m_remoteUserId(std::move(userId)) + , m_remoteDeviceId(std::move(deviceId)) , m_transactionId(QUuid::createUuid().toString()) , m_connection(connection) , m_encrypted(false) { - m_timeout = 600000; - init(); + init(600s); QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); } -void KeyVerificationSession::init() +void KeyVerificationSession::init(milliseconds timeout) { connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { @@ -85,10 +91,7 @@ void KeyVerificationSession::init() } }); - QTimer::singleShot(m_timeout, this, [this] { - cancelVerification(TIMEOUT); - }); - + QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); m_sas = olm_sas(new uint8_t[olm_sas_size()]); auto randomSize = olm_create_sas_random_length(m_sas); @@ -380,7 +383,7 @@ void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) } } -void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent& event) +void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent&) { if (state() != DONE) { cancelVerification(UNEXPECTED_MESSAGE); diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index cb7a99e9..2756fa0a 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -4,10 +4,10 @@ #pragma once #include "events/keyverificationevent.h" + #include -#include -class OlmSAS; +struct OlmSAS; namespace Quotient { class Connection; @@ -22,16 +22,20 @@ class QUOTIENT_API KeyVerificationSession : public QObject public: enum State { - INCOMING, // There is a request for verification incoming - WAITINGFORREADY, // We sent a request for verification and are waiting for ready - READY, // Either party sent a ready as a response to a request; The user selects a method - WAITINGFORACCEPT, // We sent a start and are waiting for an accept - ACCEPTED, // The other party sent an accept and is waiting for a key - WAITINGFORKEY, // We're waiting for a key - WAITINGFORVERIFICATION, // We're waiting for the *user* to verify the emojis - WAITINGFORMAC, // We're waiting for the mac - CANCELED, // The session has been canceled - DONE, // The verification is done + INCOMING, ///< There is a request for verification incoming + //! We sent a request for verification and are waiting for ready + WAITINGFORREADY, + //! Either party sent a ready as a response to a request; the user + //! selects a method + READY, + WAITINGFORACCEPT, ///< We sent a start and are waiting for an accept + ACCEPTED, ///< The other party sent an accept and is waiting for a key + WAITINGFORKEY, ///< We're waiting for a key + //! We're waiting for the *user* to verify the emojis + WAITINGFORVERIFICATION, + WAITINGFORMAC, ///< We're waiting for the mac + CANCELED, ///< The session has been canceled + DONE, ///< The verification is done }; Q_ENUM(State) @@ -60,19 +64,21 @@ public: MISMATCHED_SAS, REMOTE_MISMATCHED_SAS, }; - Q_ENUM(Error); + Q_ENUM(Error) - //Q_PROPERTY(int timeLeft READ timeLeft NOTIFY timeLeftChanged) Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) Q_PROPERTY(QList sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) Q_PROPERTY(State state READ state NOTIFY stateChanged) Q_PROPERTY(Error error READ error NOTIFY errorChanged) - KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection* connection, bool encrypted, QObject* parent = nullptr); - KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent = nullptr); - ~KeyVerificationSession(); + KeyVerificationSession(QString remoteUserId, + const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted); + KeyVerificationSession(QString userId, QString deviceId, + Connection* connection); + ~KeyVerificationSession() override; + Q_DISABLE_COPY_MOVE(KeyVerificationSession) - int timeLeft() const; QList sasEmojis() const; State state() const; @@ -88,8 +94,6 @@ public Q_SLOTS: void cancelVerification(Error error); Q_SIGNALS: - - void timeLeftChanged(); void startReceived(); void keyReceived(); void sasEmojisChanged(); @@ -98,20 +102,18 @@ Q_SIGNALS: void finished(); private: - QString m_remoteUserId; - QString m_remoteDeviceId; - QString m_transactionId; + const QString m_remoteUserId; + const QString m_remoteDeviceId; + const QString m_transactionId; Connection* m_connection; OlmSAS* m_sas = nullptr; - int timeleft = 0; QList m_sasEmojis; bool startSentByUs = false; - State m_state; - Error m_error; + State m_state = INCOMING; + Error m_error = NONE; QString m_startEvent; QString m_commitment; QString m_language; - int m_timeout; bool macReceived = false; bool m_encrypted; QStringList m_remoteSupportedMethods; @@ -121,9 +123,9 @@ private: void handleAccept(const KeyVerificationAcceptEvent& event); void handleKey(const KeyVerificationKeyEvent& event); void handleMac(const KeyVerificationMacEvent& event); - void handleDone(const KeyVerificationDoneEvent& event); + void handleDone(const KeyVerificationDoneEvent&); void handleCancel(const KeyVerificationCancelEvent& event); - void init(); + void init(std::chrono::milliseconds timeout); void setState(State state); void setError(Error error); QStringList commonSupportedMethods(const QStringList& remoteSupportedMethods) const; -- cgit v1.2.3 From 2e1f179bf75da9705963be9305ab6db34afa4d6d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 20 Aug 2022 19:18:17 +0200 Subject: Connection::Private::assembleEncryptedContent() What was partially factored out before into encryptSessionKeyEvent() is now the complete algorithm converting any event json into encrypted content. --- lib/connection.cpp | 35 +++++++++++++++++------------------ lib/events/roomkeyevent.cpp | 21 +++++++++------------ lib/events/roomkeyevent.h | 3 +-- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index fbe365de..19fc484a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -371,9 +371,9 @@ public: const OneTimeKeys &oneTimeKeyObject); QString curveKeyForUserDevice(const QString& userId, const QString& device) const; - QJsonObject encryptSessionKeyEvent(QJsonObject payloadJson, - const QString& targetUserId, - const QString& targetDeviceId) const; + QJsonObject assembleEncryptedContent(QJsonObject payloadJson, + const QString& targetUserId, + const QString& targetDeviceId) const; #endif void saveAccessTokenToKeychain() const @@ -2364,10 +2364,16 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, return true; } -QJsonObject Connection::Private::encryptSessionKeyEvent( +QJsonObject Connection::Private::assembleEncryptedContent( QJsonObject payloadJson, const QString& targetUserId, const QString& targetDeviceId) const { + payloadJson.insert(SenderKeyL, data->userId()); +// eventJson.insert("sender_device"_ls, data->deviceId()); + payloadJson.insert("keys"_ls, + QJsonObject{ + { Ed25519Key, + QString(olmAccount->identityKeys().ed25519) } }); payloadJson.insert("recipient"_ls, targetUserId); payloadJson.insert( "recipient_keys"_ls, @@ -2381,7 +2387,6 @@ QJsonObject Connection::Private::encryptSessionKeyEvent( QJsonObject { { "type"_ls, type }, { "body"_ls, QString(cipherText) } } } }; - return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519) .contentJson(); } @@ -2404,18 +2409,8 @@ void Connection::sendSessionKeyToDevices( if (hash.isEmpty()) return; - auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, roomId, sessionId, - sessionKey, userId()) - .fullJson(); - keyEventJson.insert(SenderKeyL, userId()); - keyEventJson.insert("sender_device"_ls, deviceId()); - keyEventJson.insert( - "keys"_ls, - QJsonObject { - { Ed25519Key, QString(olmAccount()->identityKeys().ed25519) } }); - auto job = callApi(hash); - connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, keyEventJson, devices, index] { + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { QHash> usersToDevicesToContent; for (const auto oneTimeKeys = job->oneTimeKeys(); const auto& [targetUserId, targetDeviceId] : @@ -2429,10 +2424,14 @@ void Connection::sendSessionKeyToDevices( // Noisy but nice for debugging // qDebug(E2EE) << "Creating the payload for" << targetUserId // << targetDeviceId << sessionId << sessionKey.toHex(); + const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, + roomId, sessionId, sessionKey) + .fullJson(); + usersToDevicesToContent[targetUserId][targetDeviceId] = - d->encryptSessionKeyEvent(keyEventJson, targetUserId, + d->assembleEncryptedContent(keyEventJson, targetUserId, targetDeviceId); - } + } if (!usersToDevicesToContent.empty()) { sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent); QVector> receivedDevices; diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 68962950..3a8601d1 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -5,21 +5,18 @@ using namespace Quotient; -RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) +RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(TypeId, obj) { if (roomId().isEmpty()) qCWarning(E2EE) << "Room key event has empty room id"; } -RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString& sessionId, const QString& sessionKey, const QString& senderId) - : Event(typeId(), { - {"content", QJsonObject{ - {"algorithm", algorithm}, - {"room_id", roomId}, - {"session_id", sessionId}, - {"session_key", sessionKey}, - }}, - {"sender", senderId}, - {"type", "m.room_key"}, - }) +RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey) + : Event(TypeId, basicJson(TypeId, { + { "algorithm", algorithm }, + { "room_id", roomId }, + { "session_id", sessionId }, + { "session_key", sessionKey }, + })) {} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 9eb2854b..0dfdf383 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -13,8 +13,7 @@ public: explicit RoomKeyEvent(const QJsonObject& obj); explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, - const QString& sessionId, const QString& sessionKey, - const QString& senderId); + const QString& sessionId, const QString& sessionKey); QUO_CONTENT_GETTER(QString, algorithm) QUO_CONTENT_GETTER(QString, roomId) -- cgit v1.2.3 From 376da43a29f3ebad807da2761e7a0c0b105587ec Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 19:58:18 +0200 Subject: More code reorganisation - Common switchOnType() piece for key verification events is factored out into processIfVerificationEvent() - Bare event JSON removed from KeyVerificationSession into constructors of respective events - Connection::sendToDevice() uses assembleEncryptedContent() introduced in the previous commit - commonSupportedMethods() moved out to .cpp; error/string converters made static --- lib/connection.cpp | 127 ++++++++++++---------------- lib/connection.h | 4 +- lib/events/keyverificationevent.h | 77 ++++++++++++++++- lib/keyverificationsession.cpp | 173 ++++++++++++++------------------------ lib/keyverificationsession.h | 8 +- 5 files changed, 198 insertions(+), 191 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 19fc484a..04cabf47 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -121,6 +121,7 @@ public: QHash oneTimeKeysCount; std::vector> pendingEncryptedEvents; void handleEncryptedToDeviceEvent(const EncryptedEvent& event); + bool processIfVerificationEvent(const Event &evt, bool encrypted); // A map from SenderKey to vector of InboundSession UnorderedMap> olmSessions; @@ -988,68 +989,71 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) pendingEncryptedEvents.push_back(std::move(event)); continue; } - switchOnType(*tdEvt, - [this](const KeyVerificationRequestEvent& event) { - auto session = new KeyVerificationSession(q->userId(), - event, q, false); - emit q->newKeyVerificationSession(session); - }, [this](const KeyVerificationReadyEvent& event) { - emit q->incomingKeyVerificationReady(event); - }, [this](const KeyVerificationStartEvent& event) { - emit q->incomingKeyVerificationStart(event); - }, [this](const KeyVerificationAcceptEvent& event) { - emit q->incomingKeyVerificationAccept(event); - }, [this](const KeyVerificationKeyEvent& event) { - emit q->incomingKeyVerificationKey(event); - }, [this](const KeyVerificationMacEvent& event) { - emit q->incomingKeyVerificationMac(event); - }, [this](const KeyVerificationDoneEvent& event) { - emit q->incomingKeyVerificationDone(event); - }, [this](const KeyVerificationCancelEvent& event) { - emit q->incomingKeyVerificationCancel(event); - }); + processIfVerificationEvent(*tdEvt, false); } } #endif } #ifdef Quotient_E2EE_ENABLED -void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) -{ - const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); - if(!decryptedEvent) { - qCWarning(E2EE) << "Failed to decrypt event" << event.id(); - return; - } - - switchOnType(*decryptedEvent, - [this, &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); - } else { - qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - } - }, [this](const KeyVerificationRequestEvent& event) { - emit q->newKeyVerificationSession( - new KeyVerificationSession(q->userId(), event, q, true)); +bool Connection::Private::processIfVerificationEvent(const Event& evt, + bool encrypted) +{ + return switchOnType(evt, + [this, encrypted](const KeyVerificationRequestEvent& event) { + auto session = + new KeyVerificationSession(q->userId(), event, q, encrypted); + emit q->newKeyVerificationSession(session); + return true; }, [this](const KeyVerificationReadyEvent& event) { emit q->incomingKeyVerificationReady(event); + return true; }, [this](const KeyVerificationStartEvent& event) { emit q->incomingKeyVerificationStart(event); + return true; }, [this](const KeyVerificationAcceptEvent& event) { emit q->incomingKeyVerificationAccept(event); + return true; }, [this](const KeyVerificationKeyEvent& event) { emit q->incomingKeyVerificationKey(event); + return true; }, [this](const KeyVerificationMacEvent& event) { emit q->incomingKeyVerificationMac(event); + return true; }, [this](const KeyVerificationDoneEvent& event) { emit q->incomingKeyVerificationDone(event); + return true; }, [this](const KeyVerificationCancelEvent& event) { emit q->incomingKeyVerificationCancel(event); - }, [](const Event& evt) { + return true; + }, false); +} + +void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) +{ + const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } + + if (processIfVerificationEvent(*decryptedEvent, true)) + return; + switchOnType(*decryptedEvent, + [this, &event, + olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), + olmSessionId); + } else { + qCDebug(E2EE) + << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { qCWarning(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); + << evt.matrixType(); }); } #endif @@ -2466,37 +2470,16 @@ void Connection::startKeyVerificationSession(const QString& deviceId) emit newKeyVerificationSession(session); } -void Connection::sendToDevice(const QString& userId, const QString& deviceId, - event_ptr_tt event, bool encrypted) -{ - if (encrypted) { - QJsonObject payloadJson = event->fullJson(); - payloadJson["recipient"] = userId; - payloadJson["sender"] = user()->id(); - QJsonObject recipientObject; - recipientObject["ed25519"] = edKeyForUserDevice(userId, deviceId); - payloadJson["recipient_keys"] = recipientObject; - QJsonObject senderObject; - senderObject["ed25519"] = QString(olmAccount()->identityKeys().ed25519); - payloadJson["keys"] = senderObject; - - auto cipherText = d->olmEncryptMessage( - userId, deviceId, - QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); - QJsonObject encryptedJson; - encryptedJson[d->curveKeyForUserDevice(userId, deviceId)] = - QJsonObject{ { "type", cipherText.first }, - { "body", QString(cipherText.second) }, - { "sender", this->userId() } }; - const auto& contentJson = - EncryptedEvent(encryptedJson, - olmAccount()->identityKeys().curve25519) - .contentJson(); - sendToDevices(EncryptedEvent::TypeId, - { { userId, { { deviceId, contentJson } } } }); - } else - sendToDevices(event->matrixType(), - { { userId, { { deviceId, event->contentJson() } } } }); +void Connection::sendToDevice(const QString& targetUserId, + const QString& targetDeviceId, Event event, + bool encrypted) +{ + const auto contentJson = + encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId, + targetDeviceId) + : event.contentJson(); + sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(), + { { targetUserId, { { targetDeviceId, contentJson } } } }); } bool Connection::isVerifiedSession(const QString& megolmSessionId) const diff --git a/lib/connection.h b/lib/connection.h index 3a4ee798..5fdc525d 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -336,8 +336,8 @@ public: bool hasOlmSession(const QString& user, const QString& deviceId) const; // This assumes that an olm session already exists. If it doesn't, no message is sent. - void sendToDevice(const QString& userId, const QString& deviceId, - event_ptr_tt event, bool encrypted); + void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, + Event event, bool encrypted); /// Returns true if this megolm session comes from a verified device bool isVerifiedSession(const QString& megolmSessionId) const; diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index cdbd5d74..f635d07b 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -18,6 +18,16 @@ public: explicit KeyVerificationRequestEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationRequestEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods, + const QDateTime& timestamp) + : KeyVerificationRequestEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) }, + { "timestamp"_ls, toJson(timestamp) } })) + {} /// The device ID which is initiating the request. QUO_CONTENT_GETTER(QString, fromDevice) @@ -44,6 +54,14 @@ public: explicit KeyVerificationReadyEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationReadyEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods) + : KeyVerificationReadyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) } })) + {} /// The device ID which is accepting the request. QUO_CONTENT_GETTER(QString, fromDevice) @@ -62,9 +80,23 @@ class QUOTIENT_API KeyVerificationStartEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) - explicit KeyVerificationStartEvent(const QJsonObject &obj) + explicit KeyVerificationStartEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationStartEvent(const QString& transactionId, + const QString& fromDevice) + : KeyVerificationStartEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "method"_ls, SasV1Method }, + { "hashes"_ls, QJsonArray{ "sha256"_ls } }, + { "key_agreement_protocols"_ls, + QJsonArray{ "curve25519-hkdf-sha256"_ls } }, + { "message_authentication_codes"_ls, + QJsonArray{ "hkdf-hmac-sha256"_ls } }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls } } })) + {} /// The device ID which is initiating the process. QUO_CONTENT_GETTER(QString, fromDevice) @@ -125,6 +157,18 @@ public: explicit KeyVerificationAcceptEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationAcceptEvent(const QString& transactionId, + const QString& commitment) + : KeyVerificationAcceptEvent(basicJson( + TypeId, { { "transaction_id"_ls, transactionId }, + { "method"_ls, SasV1Method }, + { "key_agreement_protocol"_ls, "curve25519-hkdf-sha256" }, + { "hash"_ls, "sha256" }, + { "message_authentication_code"_ls, "hkdf-hmac-sha256" }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls, } }, + { "commitment"_ls, commitment } })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -161,9 +205,18 @@ class QUOTIENT_API KeyVerificationCancelEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) - explicit KeyVerificationCancelEvent(const QJsonObject &obj) + explicit KeyVerificationCancelEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationCancelEvent(const QString& transactionId, + const QString& reason) + : KeyVerificationCancelEvent( + basicJson(TypeId, { + { "transaction_id"_ls, transactionId }, + { "reason"_ls, reason }, + { "code"_ls, reason } // Not a typo + })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -183,9 +236,14 @@ class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) - explicit KeyVerificationKeyEvent(const QJsonObject &obj) + explicit KeyVerificationKeyEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationKeyEvent(const QString& transactionId, const QString& key) + : KeyVerificationKeyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "key"_ls, key } })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -200,9 +258,16 @@ class QUOTIENT_API KeyVerificationMacEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) - explicit KeyVerificationMacEvent(const QJsonObject &obj) + explicit KeyVerificationMacEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + KeyVerificationMacEvent(const QString& transactionId, const QString& keys, + const QJsonObject& mac) + : KeyVerificationMacEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "keys"_ls, keys }, + { "mac"_ls, mac } })) + {} /// An opaque identifier for the verification process. QUO_CONTENT_GETTER(QString, transactionId) @@ -224,6 +289,10 @@ public: explicit KeyVerificationDoneEvent(const QJsonObject& obj) : Event(TypeId, obj) {} + explicit KeyVerificationDoneEvent(const QString& transactionId) + : KeyVerificationDoneEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId } })) + {} /// The same transactionId as before QUO_CONTENT_GETTER(QString, transactionId) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 1ee489ea..caf5071a 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -20,6 +20,19 @@ using namespace Quotient; using namespace std::chrono; +const QStringList supportedMethods = { SasV1Method }; + +QStringList commonSupportedMethods(const QStringList& remoteMethods) +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (supportedMethods.contains(method)) { + result += method; + } + } + return result; +} + KeyVerificationSession::KeyVerificationSession( QString remoteUserId, const KeyVerificationRequestEvent& event, Connection* connection, bool encrypted) @@ -93,7 +106,7 @@ void KeyVerificationSession::init(milliseconds timeout) QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); - m_sas = olm_sas(new uint8_t[olm_sas_size()]); + m_sas = olm_sas(new std::byte[olm_sas_size()]); auto randomSize = olm_create_sas_random_length(m_sas); auto random = getRandom(randomSize); olm_create_sas(m_sas, random.data(), randomSize); @@ -104,7 +117,8 @@ void KeyVerificationSession::init(milliseconds timeout) KeyVerificationSession::~KeyVerificationSession() { - delete[] reinterpret_cast(m_sas); + olm_clear_sas(m_sas); + delete[] reinterpret_cast(m_sas); } void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) @@ -160,18 +174,22 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) emit keyReceived(); } -QByteArray KeyVerificationSession::macInfo(bool verifying, const QString& key) -{ - return (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls).arg(m_connection->userId()).arg(m_connection->deviceId()).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(m_transactionId).arg(key).toLatin1(); -} - -QString KeyVerificationSession::calculateMac(const QString& input, bool verifying, const QString& keyId) +QString KeyVerificationSession::calculateMac(const QString& input, + bool verifying, + const QString& keyId) { QByteArray inputBytes = input.toLatin1(); QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); - olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), macInfo(verifying, keyId).data(), macInfo(verifying, keyId).size(), outputBytes.data(), outputBytes.size()); - auto output = QString(outputBytes); - return output.left(output.indexOf('=')); + const auto macInfo = + (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls + : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls) + .arg(m_connection->userId(), m_connection->deviceId(), + m_remoteUserId, m_remoteDeviceId, m_transactionId, keyId) + .toLatin1(); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), + macInfo.data(), macInfo.size(), outputBytes.data(), + outputBytes.size()); + return QString::fromLatin1(outputBytes.data(), outputBytes.indexOf('=')); } void KeyVerificationSession::sendMac() @@ -184,56 +202,37 @@ void KeyVerificationSession::sendMac() auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; mac[edKeyId] = calculateMac(key, false, edKeyId); - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.mac"}, - {"content", QJsonObject{ - {"transaction_id", m_transactionId}, - {"keys", keys}, - {"mac", mac}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationMacEvent(m_transactionId, keys, + mac), + m_encrypted); setState (macReceived ? DONE : WAITINGFORMAC); } void KeyVerificationSession::sendDone() { - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.done"}, - {"content", QJsonObject{ - {"transaction_id", m_transactionId}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationDoneEvent(m_transactionId), + m_encrypted); } void KeyVerificationSession::sendKey() { QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); - QString key = QString(keyBytes); - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.key"}, - {"content", QJsonObject{ - {"transaction_id", m_transactionId}, - {"key", key}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationKeyEvent(m_transactionId, + keyBytes), + m_encrypted); } void KeyVerificationSession::cancelVerification(Error error) { - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.cancel"}, - {"content", QJsonObject{ - {"code", errorToString(error)}, - {"reason", errorToString(error)}, - {"transaction_id", m_transactionId} - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationCancelEvent(m_transactionId, + errorToString(error)), + m_encrypted); setState(CANCELED); setError(error); emit finished(); @@ -249,15 +248,11 @@ void KeyVerificationSession::sendReady() return; } - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.ready"}, - {"content", QJsonObject { - {"from_device", m_connection->deviceId()}, - {"methods", toJson(methods)}, - {"transaction_id", m_transactionId}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), + methods), + m_encrypted); setState(READY); if (methods.size() == 1) { @@ -268,20 +263,10 @@ void KeyVerificationSession::sendReady() void KeyVerificationSession::sendStartSas() { startSentByUs = true; - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.start"}, - {"content", QJsonObject { - {"from_device", m_connection->deviceId()}, - {"hashes", QJsonArray {"sha256"}}, - {"key_agreement_protocols", QJsonArray { "curve25519-hkdf-sha256" }}, - {"message_authentication_codes", QJsonArray { "hkdf-hmac-sha256" }}, - {"method", "m.sas.v1"}, - {"short_authentication_string", QJsonArray { "decimal", "emoji" }}, - {"transaction_id", m_transactionId}, - }} - }); - m_startEvent = QJsonDocument(event->contentJson()).toJson(QJsonDocument::Compact); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); + m_startEvent = QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + std::move(event), m_encrypted); setState(WAITINGFORACCEPT); } @@ -324,22 +309,10 @@ void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); commitment = commitment.left(commitment.indexOf('=')); - auto acceptEvent = makeEvent(QJsonObject { - {"type", "m.key.verification.accept"}, - {"content", QJsonObject { - {"commitment", commitment}, - {"hash", "sha256"}, - {"key_agreement_protocol", "curve25519-hkdf-sha256"}, - {"message_authentication_code", "hkdf-hmac-sha256"}, - {"method", "m.sas.v1"}, - {"short_authentication_string", QJsonArray { - "decimal", - "emoji", - }}, - {"transaction_id", m_transactionId}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(acceptEvent), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationAcceptEvent(m_transactionId, + commitment), + m_encrypted); setState(ACCEPTED); } @@ -417,17 +390,12 @@ QList KeyVerificationSession::sasEmojis() const void KeyVerificationSession::sendRequest() { - QJsonArray methods = toJson(m_supportedMethods); - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.request"}, - {"content", QJsonObject { - {"from_device", m_connection->deviceId()}, - {"transaction_id", m_transactionId}, - {"methods", methods}, - {"timestamp", QDateTime::currentMSecsSinceEpoch()}, - }}, - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), + supportedMethods, + QDateTime::currentDateTime()), + m_encrypted); setState(WAITINGFORREADY); } @@ -453,7 +421,7 @@ void KeyVerificationSession::setError(Error error) emit errorChanged(); } -QString KeyVerificationSession::errorToString(Error error) const +QString KeyVerificationSession::errorToString(Error error) { switch(error) { case NONE: @@ -485,7 +453,7 @@ QString KeyVerificationSession::errorToString(Error error) const } } -KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) const +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) { if (error == "m.timeout"_ls) { return REMOTE_TIMEOUT; @@ -514,14 +482,3 @@ KeyVerificationSession::Error KeyVerificationSession::stringToError(const QStrin } return NONE; } - -QStringList KeyVerificationSession::commonSupportedMethods(const QStringList& remoteMethods) const -{ - QStringList result; - for (const auto& method : remoteMethods) { - if (m_supportedMethods.contains(method)) { - result += method; - } - } - return result; -} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index 2756fa0a..73c9384e 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -128,10 +128,8 @@ private: void init(std::chrono::milliseconds timeout); void setState(State state); void setError(Error error); - QStringList commonSupportedMethods(const QStringList& remoteSupportedMethods) const; - QString errorToString(Error error) const; - Error stringToError(const QString& error) const; - QStringList m_supportedMethods = { "m.sas.v1"_ls }; + static QString errorToString(Error error); + static Error stringToError(const QString& error); QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); @@ -139,4 +137,4 @@ private: std::pair emojiForCode(int code); }; -} +} // namespace Quotient -- cgit v1.2.3 From 6404b8cd4d57468b810538da04f8017fb13ccc37 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 19:58:48 +0200 Subject: Refactor the code handling emoji - Use a dedicated structure, EmojiEntry, instead of QVariantMap (it's Q_GADGET, should be readable from QML just fine) - While we're at it, QVector is better than QList in Qt 5.15 - Remove language from the session state - it's used in a single method - Modernise handleKey() code --- lib/keyverificationsession.cpp | 128 +++++++++++++++++++++++++---------------- lib/keyverificationsession.h | 19 ++++-- 2 files changed, 91 insertions(+), 56 deletions(-) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index caf5071a..2c468c3e 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -110,9 +110,6 @@ void KeyVerificationSession::init(milliseconds timeout) auto randomSize = olm_create_sas_random_length(m_sas); auto random = getRandom(randomSize); olm_create_sas(m_sas, random.data(), randomSize); - - m_language = QLocale::system().uiLanguages()[0]; - m_language = m_language.left(m_language.indexOf('-')); } KeyVerificationSession::~KeyVerificationSession() @@ -121,18 +118,57 @@ KeyVerificationSession::~KeyVerificationSession() delete[] reinterpret_cast(m_sas); } +struct EmojiStoreEntry : EmojiEntry { + QHash translatedDescriptions; + + explicit EmojiStoreEntry(const QJsonObject& json) + : EmojiEntry{ fromJson(json["emoji"]), + fromJson(json["description"]) } + , translatedDescriptions{ fromJson>( + json["translated_descriptions"]) } + {} +}; + +using EmojiStore = QVector; + +EmojiStore loadEmojiStore() +{ + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + return fromJson( + QJsonDocument::fromJson(dataFile.readAll()).array()); +} + +EmojiEntry emojiForCode(int code, const QString& language) +{ + static const EmojiStore emojiStore = loadEmojiStore(); + const auto& entry = emojiStore[code]; + if (!language.isEmpty()) + if (const auto translatedDescription = + emojiStore[code].translatedDescriptions.value(language); + !translatedDescription.isNull()) + return { entry.emoji, translatedDescription }; + + return SLICE(entry, EmojiEntry); +} + void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) { if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { cancelVerification(UNEXPECTED_MESSAGE); return; } - olm_sas_set_their_key(m_sas, event.key().toLatin1().data(), event.key().toLatin1().size()); + auto eventKey = event.key().toLatin1(); + olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); if (startSentByUs) { - auto commitment = QString(QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); - commitment = commitment.left(commitment.indexOf('=')); - if (commitment != m_commitment) { + const auto paddedCommitment = + QCryptographicHash::hash((eventKey % m_startEvent).toLatin1(), + QCryptographicHash::Sha256) + .toBase64(); + const QLatin1String unpaddedCommitment(paddedCommitment.constData(), + paddedCommitment.indexOf('=')); + if (unpaddedCommitment != m_commitment) { qCWarning(E2EE) << "Commitment mismatch; aborting verification"; cancelVerification(MISMATCHED_COMMITMENT); return; @@ -142,34 +178,40 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) } setState(WAITINGFORVERIFICATION); - QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); - olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); - QString key = QString(keyBytes); - - QByteArray output(6, '\0'); - QString infoTemplate = startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; - - auto info = infoTemplate.arg(m_connection->userId()).arg(m_connection->deviceId()).arg(key).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(event.key()).arg(m_transactionId); - olm_sas_generate_bytes(m_sas, info.toLatin1().data(), info.toLatin1().size(), output.data(), output.size()); - - QVector code(7, 0); - const auto& data = (uint8_t *) output.data(); - - code[0] = data[0] >> 2; - code[1] = (data[0] << 4 & 0x3f) | data[1] >> 4; - code[2] = (data[1] << 2 & 0x3f) | data[2] >> 6; - code[3] = data[2] & 0x3f; - code[4] = data[3] >> 2; - code[5] = (data[3] << 4 & 0x3f) | data[4] >> 4; - code[6] = (data[4] << 2 & 0x3f) | data[5] >> 6; - - for (const auto& c : code) { - auto [emoji, description] = emojiForCode(c); - QVariantMap map; - map["emoji"] = emoji; - map["description"] = description; - m_sasEmojis += map; - } + std::string key(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, key.data(), key.size()); + + std::array output{}; + const auto infoTemplate = + startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls + : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + const auto info = infoTemplate + .arg(m_connection->userId(), m_connection->deviceId(), + key.data(), m_remoteUserId, m_remoteDeviceId, + eventKey, m_transactionId) + .toLatin1(); + olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(), + output.size()); + + static constexpr auto x3f = std::byte{ 0x3f }; + const std::array code{ + output[0] >> 2, + (output[0] << 4 & x3f) | output[1] >> 4, + (output[1] << 2 & x3f) | output[2] >> 6, + output[2] & x3f, + output[3] >> 2, + (output[3] << 4 & x3f) | output[4] >> 4, + (output[4] << 2 & x3f) | output[5] >> 6 + }; + + const auto uiLanguages = QLocale().uiLanguages(); + const auto preferredLanguage = uiLanguages.isEmpty() + ? QString() + : uiLanguages.front().section('-', 0, 0); + for (const auto& c : code) + m_sasEmojis += emojiForCode(std::to_integer(c), preferredLanguage); + emit sasEmojisChanged(); emit keyReceived(); } @@ -369,21 +411,7 @@ void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& even setState(CANCELED); } -std::pair KeyVerificationSession::emojiForCode(int code) -{ - static QJsonArray data; - if (data.isEmpty()) { - QFile dataFile(":/sas-emoji.json"); - dataFile.open(QFile::ReadOnly); - data = QJsonDocument::fromJson(dataFile.readAll()).array(); - } - if (data[code].toObject()["translated_descriptions"].toObject().contains(m_language)) { - return {data[code].toObject()["emoji"].toString(), data[code].toObject()["translated_descriptions"].toObject()[m_language].toString()}; - } - return {data[code].toObject()["emoji"].toString(), data[code].toObject()["description"].toString()}; -} - -QList KeyVerificationSession::sasEmojis() const +QVector KeyVerificationSession::sasEmojis() const { return m_sasEmojis; } diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index 73c9384e..aa0295cb 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -12,6 +12,15 @@ struct OlmSAS; namespace Quotient { class Connection; +struct QUOTIENT_API EmojiEntry { + QString emoji; + QString description; + + Q_GADGET + Q_PROPERTY(QString emoji MEMBER emoji CONSTANT) + Q_PROPERTY(QString description MEMBER description CONSTANT) +}; + /** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. Start a new session using Connection::startKeyVerificationSession. The object is delete after finished is emitted. @@ -67,7 +76,7 @@ public: Q_ENUM(Error) Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) - Q_PROPERTY(QList sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) + Q_PROPERTY(QVector sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) Q_PROPERTY(State state READ state NOTIFY stateChanged) Q_PROPERTY(Error error READ error NOTIFY errorChanged) @@ -79,7 +88,7 @@ public: ~KeyVerificationSession() override; Q_DISABLE_COPY_MOVE(KeyVerificationSession) - QList sasEmojis() const; + QVector sasEmojis() const; State state() const; Error error() const; @@ -107,13 +116,12 @@ private: const QString m_transactionId; Connection* m_connection; OlmSAS* m_sas = nullptr; - QList m_sasEmojis; + QVector m_sasEmojis; bool startSentByUs = false; State m_state = INCOMING; Error m_error = NONE; QString m_startEvent; QString m_commitment; - QString m_language; bool macReceived = false; bool m_encrypted; QStringList m_remoteSupportedMethods; @@ -133,8 +141,7 @@ private: QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); - - std::pair emojiForCode(int code); }; } // namespace Quotient +Q_DECLARE_METATYPE(Quotient::EmojiEntry) -- cgit v1.2.3 From e89986123c1bc55ceccb28c417c33ecdca602512 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 20:03:09 +0200 Subject: .clang-format: put e2ee/ headers in their own section [skip ci] --- .clang-format | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.clang-format b/.clang-format index 70864160..d4a3d2cc 100644 --- a/.clang-format +++ b/.clang-format @@ -93,6 +93,8 @@ IncludeCategories: Priority: 32 - Regex: '"csapi/' Priority: 2 + - Regex: '"e2ee/' + Priority: 3 - Regex: '"(events|jobs)/' Priority: 4 - Regex: '.*' -- 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/connection.cpp | 7 +++---- lib/connection.h | 13 ++----------- lib/room.cpp | 1 + quotest/quotest.cpp | 1 + 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 9e4444df..98720f3b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -6,11 +6,12 @@ #include "connection.h" +#include "accountregistry.h" #include "connectiondata.h" +#include "qt_connection_util.h" #include "room.h" #include "settings.h" #include "user.h" -#include "accountregistry.h" // NB: since Qt 6, moc_connection.cpp needs Room and User fully defined #include "moc_connection.cpp" @@ -20,10 +21,8 @@ #include "csapi/joining.h" #include "csapi/leaving.h" #include "csapi/logout.h" -#include "csapi/receipts.h" #include "csapi/room_send.h" #include "csapi/to_device.h" -#include "csapi/versions.h" #include "csapi/voip.h" #include "csapi/wellknown.h" #include "csapi/whoami.h" @@ -2184,8 +2183,8 @@ PicklingMode Connection::picklingMode() const void Connection::saveOlmAccount() { - qCDebug(E2EE) << "Saving olm account"; #ifdef Quotient_E2EE_ENABLED + qCDebug(E2EE) << "Saving olm account"; if (const auto expectedPickle = d->olmAccount->pickle(d->picklingMode)) d->database->setAccountPickle(*expectedPickle); else diff --git a/lib/connection.h b/lib/connection.h index 0d0c85b6..0e0abc39 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -5,13 +5,12 @@ #pragma once -#include "ssosession.h" -#include "qt_connection_util.h" #include "quotient_common.h" +#include "ssosession.h" #include "util.h" -#include "csapi/login.h" #include "csapi/create_room.h" +#include "csapi/login.h" #include "events/accountdataevents.h" @@ -379,20 +378,12 @@ public: /** * Call this before first sync to load from previously saved file. - * - * \param fromFile A local path to read the state from. Uses QUrl - * to be QML-friendly. Empty parameter means saving to the directory - * defined by stateCachePath() / stateCacheDir(). */ Q_INVOKABLE void loadState(); /** * This method saves the current state of rooms (but not messages * in them) to a local cache file, so that it could be loaded by * loadState() on a next run of the client. - * - * \param toFile A local path to save the state to. Uses QUrl to be - * QML-friendly. Empty parameter means saving to the directory - * defined by stateCachePath() / stateCacheDir(). */ Q_INVOKABLE void saveState() const; 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" diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 90a5a69b..b6c855bb 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -6,6 +6,7 @@ #include "user.h" #include "uriresolver.h" #include "networkaccessmanager.h" +#include "qt_connection_util.h" #include "csapi/joining.h" #include "csapi/leaving.h" -- cgit v1.2.3 From d1a4bbf693ad14f2ea0a308fef61db95161a17b7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 26 Aug 2022 23:36:39 +0200 Subject: Fix device verification QByteArrays don't like interacting with QStrings --- lib/keyverificationsession.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 2c468c3e..44a085fe 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -163,7 +163,7 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) if (startSentByUs) { const auto paddedCommitment = - QCryptographicHash::hash((eventKey % m_startEvent).toLatin1(), + QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), QCryptographicHash::Sha256) .toBase64(); const QLatin1String unpaddedCommitment(paddedCommitment.constData(), @@ -189,7 +189,7 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) const auto info = infoTemplate .arg(m_connection->userId(), m_connection->deviceId(), key.data(), m_remoteUserId, m_remoteDeviceId, - eventKey, m_transactionId) + event.key(), m_transactionId) .toLatin1(); olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(), output.size()); -- cgit v1.2.3 From 5efbf75c8719407549e4bcfcf4c5329b37630990 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 3 Sep 2022 15:26:07 +0200 Subject: KeyVerificationDoneEvent: fix copy-pasta in DEFINE_EVENT_TYPEID --- lib/events/keyverificationevent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index f635d07b..5b587522 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -284,7 +284,7 @@ REGISTER_EVENT_TYPE(KeyVerificationMacEvent) class QUOTIENT_API KeyVerificationDoneEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationRequestEvent) + DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationDoneEvent) explicit KeyVerificationDoneEvent(const QJsonObject& obj) : Event(TypeId, obj) -- cgit v1.2.3 From 8cb629f406f5b8b1ff7ce787dd3967d5684e07c3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 4 Sep 2022 18:40:54 +0200 Subject: Fix a typo in cacheLocation() doc-comment [skip ci] --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index 46b1767e..1cb1c0e1 100644 --- a/lib/util.h +++ b/lib/util.h @@ -210,7 +210,7 @@ QUOTIENT_API QString prettyPrint(const QString& plainText); /** Return a path to cache directory after making sure that it exists * * The returned path has a trailing slash, clients don't need to append it. - * \param dir path to cache directory relative to the standard cache path + * \param dirName path to cache directory relative to the standard cache path */ QUOTIENT_API QString cacheLocation(const QString& dirName); -- cgit v1.2.3 From dbc78d185c4bafe03b458f9eeb7ef3af35ce2eb2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 21 Jun 2022 07:33:43 +0200 Subject: SingleKeyValue: allow seamless construction from the underlying type SingleKeyValue is a tiny wrapper and supposed to be discreet. Having to explicitly (even if only with braces) construct its objects stands in the way of readability on the consuming side of the code and sometimes prevents direct initialisation of event objects without constructors getting some kind of ContentParamTs parameter pack where a single content_type argument would suffice otherwise. --- lib/events/single_key_value.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h index 75ca8cd2..5edff3b1 100644 --- a/lib/events/single_key_value.h +++ b/lib/events/single_key_value.h @@ -7,6 +7,15 @@ namespace Quotient { namespace EventContent { template struct SingleKeyValue { + // NOLINTBEGIN(google-explicit-constructor): that check should learn + // about explicit(false) + QUO_IMPLICIT SingleKeyValue(const T& v = {}) + : value { v } + {} + QUO_IMPLICIT SingleKeyValue(T&& v) + : value { std::move(v) } + {} + // NOLINTEND(google-explicit-constructor) T value; }; } // namespace EventContent -- 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 --- CMakeLists.txt | 2 +- lib/connection.cpp | 2 +- lib/connection.h | 2 +- lib/events/accountdataevents.h | 18 +++++++++++------ lib/events/event.h | 46 ++++++++++++++++++++++-------------------- lib/events/reactionevent.h | 17 ++-------------- lib/events/typingevent.cpp | 11 ---------- lib/events/typingevent.h | 10 +-------- lib/room.cpp | 2 +- quotest/quotest.cpp | 3 ++- 10 files changed, 45 insertions(+), 68 deletions(-) delete mode 100644 lib/events/typingevent.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f8667645..06e754aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,7 +154,7 @@ list(APPEND lib_SRCS lib/events/roomcanonicalaliasevent.h lib/events/roomavatarevent.h lib/events/roompowerlevelsevent.h lib/events/roompowerlevelsevent.cpp - lib/events/typingevent.h lib/events/typingevent.cpp + lib/events/typingevent.h lib/events/accountdataevents.h lib/events/receiptevent.h lib/events/receiptevent.cpp lib/events/reactionevent.h diff --git a/lib/connection.cpp b/lib/connection.cpp index 79d7ae55..722829e8 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1701,7 +1701,7 @@ bool Connection::isIgnored(const User* user) const IgnoredUsersList Connection::ignoredUsers() const { const auto* event = accountData(); - return event ? event->ignored_users() : IgnoredUsersList(); + return event ? event->ignoredUsers() : IgnoredUsersList(); } void Connection::addToIgnoredUsers(const User* user) diff --git a/lib/connection.h b/lib/connection.h index 39921938..66ed8b68 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -111,7 +111,7 @@ auto defaultUserFactory(Connection* c, const QString& id) // are stored with no regard to their state. using DirectChatsMap = QMultiHash; using DirectChatUsersMap = QMultiHash; -using IgnoredUsersList = IgnoredUsersEvent::content_type; +using IgnoredUsersList = IgnoredUsersEvent::value_type; class QUOTIENT_API Connection : public QObject { Q_OBJECT diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 24c3353c..324ce449 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -4,7 +4,6 @@ #pragma once #include "event.h" -#include "util.h" namespace Quotient { constexpr auto FavouriteTag [[maybe_unused]] = "m.favourite"_ls; @@ -46,14 +45,21 @@ struct JsonObjectConverter { using TagsMap = QHash; -DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags) -DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, eventId) +DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags, "tags") +DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, + eventId, "event_id") class ReadMarkerEvent : public ReadMarkerEventImpl { public: using ReadMarkerEventImpl::ReadMarkerEventImpl; [[deprecated("Use ReadMarkerEvent::eventId() instead")]] - QString event_id() const { return eventId(); } + auto event_id() const { return eventId(); } +}; +DEFINE_SIMPLE_EVENT(IgnoredUsersEventImpl, Event, "m.ignored_user_list", + QSet, ignoredUsers, "ignored_users") +class IgnoredUsersEvent : public IgnoredUsersEventImpl { +public: + using IgnoredUsersEventImpl::IgnoredUsersEventImpl; + [[deprecated("Use IgnoredUsersEvent::ignoredUsers() instead")]] + auto ignored_users() const { return ignoredUsers(); } }; -DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, Event, "m.ignored_user_list", QSet, - ignored_users) } // namespace Quotient diff --git a/lib/events/event.h b/lib/events/event.h index b7454337..711f8fd4 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -6,6 +6,7 @@ #include "converters.h" #include "logging.h" #include "function_traits.h" +#include "single_key_value.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === @@ -258,6 +259,13 @@ template using EventsArray = std::vector>; using Events = EventsArray; +#define QUO_CONTENT_GETTER_X(PartType_, PartName_, JsonKey_) \ + PartType_ PartName_() const \ + { \ + static const auto PartName_##JsonKey = JsonKey_; \ + return contentPart(PartName_##JsonKey); \ + } + //! \brief Define an inline method obtaining a content part //! //! This macro adds a const method that extracts a JSON value at the key @@ -266,12 +274,8 @@ using Events = EventsArray; //! \code //! contentPart(Quotient::toSnakeCase(#PartName_##_ls)); //! \endcode -#define QUO_CONTENT_GETTER(PartType_, PartName_) \ - PartType_ PartName_() const \ - { \ - static const auto JsonKey = toSnakeCase(#PartName_##_ls); \ - return contentPart(JsonKey); \ - } +#define QUO_CONTENT_GETTER(PartType_, PartName_) \ + QUO_CONTENT_GETTER_X(PartType_, PartName_, toSnakeCase(#PartName_##_ls)) // === Facilities for event class definitions === @@ -301,22 +305,20 @@ using Events = EventsArray; /// To retrieve the value the getter uses a JSON key name that corresponds to /// its own (getter's) name but written in snake_case. \p GetterName_ must be /// in camelCase, no quotes (an identifier, not a literal). -#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_) \ - class QUOTIENT_API Name_ : public Base_ { \ - public: \ - using content_type = ValueType_; \ - DEFINE_EVENT_TYPEID(TypeId_, Name_) \ - explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \ - explicit Name_(const content_type& content) \ - : Name_(Base_::basicJson(TypeId, { { JsonKey, toJson(content) } })) \ - {} \ - auto GetterName_() const \ - { \ - return contentPart(JsonKey); \ - } \ - static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \ - }; \ - REGISTER_EVENT_TYPE(Name_) \ +#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ + JsonKey_) \ + class QUOTIENT_API Name_ : public Base_ { \ + public: \ + DEFINE_EVENT_TYPEID(TypeId_, Name_) \ + using value_type = ValueType_; \ + explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \ + explicit Name_(const value_type& v) \ + : Name_(Base_::basicJson(TypeId, { { JsonKey, toJson(v) } })) \ + {} \ + QUO_CONTENT_GETTER_X(ValueType_, GetterName_, JsonKey) \ + static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \ + }; \ + REGISTER_EVENT_TYPE(Name_) \ // End of macro // === is<>(), eventCast<>() and switchOnType<>() === diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index b3cb3ca7..8d873441 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -8,20 +8,7 @@ namespace Quotient { -class QUOTIENT_API ReactionEvent : public RoomEvent { -public: - DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) - - explicit ReactionEvent(const EventRelation& value) - : RoomEvent(typeId(), matrixTypeId(), - { { QStringLiteral("m.relates_to"), toJson(value) } }) - {} - explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} - EventRelation relation() const - { - return contentPart(RelatesToKey); - } -}; -REGISTER_EVENT_TYPE(ReactionEvent) +DEFINE_SIMPLE_EVENT(ReactionEvent, RoomEvent, "m.reaction", EventRelation, + relation, "m.relates_to") } // namespace Quotient diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp deleted file mode 100644 index 7e5d7ee6..00000000 --- a/lib/events/typingevent.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Kitsune Ral -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "typingevent.h" - -using namespace Quotient; - -QStringList TypingEvent::users() const -{ - return contentPart("user_ids"_ls); -} diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 522f7e42..b56475af 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -6,13 +6,5 @@ #include "event.h" namespace Quotient { -class QUOTIENT_API TypingEvent : public Event { -public: - DEFINE_EVENT_TYPEID("m.typing", TypingEvent) - - explicit TypingEvent(const QJsonObject& obj) : Event(typeId(), obj) {} - - QStringList users() const; -}; -REGISTER_EVENT_TYPE(TypingEvent) +DEFINE_SIMPLE_EVENT(TypingEvent, Event, "m.typing", QStringList, users, "user_ids") } // namespace Quotient 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()]; diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index b6c855bb..3860ae1e 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -524,7 +524,8 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return true; } -DEFINE_SIMPLE_EVENT(CustomEvent, RoomEvent, "quotest.custom", int, testValue) +DEFINE_SIMPLE_EVENT(CustomEvent, RoomEvent, "quotest.custom", int, testValue, + "test_value") TEST_IMPL(sendCustomEvent) { -- cgit v1.2.3 From a18f505fe7ca66556d66538a7c9b9ff31d2c1b29 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 9 Aug 2022 18:42:24 +0200 Subject: EventMetaType, QUO_EVENT, QUO_BASE_EVENT The new metatype framework replaces EventFactory/DEFINE_EVENT_TYPEID/REGISTER_EVENT_TYPE; it is faster, more functional and extensible. Of note: - EventMetaType mostly reproduces the logic of EventFactory but supports custom base event types not just for loading (that part EventFactory also supported) but also for matching - previously you had to have Event::is*Event() for base type matching. Now Quotient::is() can match against both base and leaf types. - Instead of DEFINE_EVENT_TYPEID and REGISTER_EVENT_TYPE there's now a single macro, QUO_EVENT, intended for use in the way similar to Q_OBJECT. Actually, the entire framework borrows heavily from QMetaObject and Q_OBJECT. Making event types full-fledged QObjects is still not considered because half of QObject functions would not be applicable (e.g. signals/slots) while another half (in particular, using Matrix type ids to select event types) would still have to be done on top of QObject. And QML can just access events as const QJsonObjects which is arguably more lightweight as well. - QUO_BASE_EVENT is a new macro replacing EventFactory object definitions. This was necessary for the same reason why Q_OBJECT is a macro: aside from a static object definition, this macro introduces a virtual function override to resolve the metatype at runtime. This very mechanism is used to make event type matching/casting as quick as possible - QUO_BASE_EVENT and QUO_EVENT use the C++20 __VA_OPT__ feature that is only available with the new MSVC preprocessor (see https://docs.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview); the respective switch was added to CMakeLists.txt. --- CMakeLists.txt | 2 +- lib/events/callanswerevent.h | 3 +- lib/events/callcandidatesevent.h | 4 +- lib/events/callhangupevent.h | 4 +- lib/events/callinviteevent.h | 4 +- lib/events/directchatevent.h | 3 +- lib/events/encryptedevent.h | 4 +- lib/events/encryptionevent.h | 3 +- lib/events/event.cpp | 41 +++- lib/events/event.h | 355 ++++++++++++++++++++++++----------- lib/events/keyverificationevent.h | 26 +-- lib/events/receiptevent.h | 3 +- lib/events/redactionevent.h | 3 +- lib/events/roomavatarevent.h | 3 +- lib/events/roomcanonicalaliasevent.h | 3 +- lib/events/roomcreateevent.h | 3 +- lib/events/roomevent.h | 16 +- lib/events/roomkeyevent.h | 3 +- lib/events/roommemberevent.h | 12 +- lib/events/roommessageevent.h | 4 +- lib/events/roompowerlevelsevent.h | 3 +- lib/events/roomtombstoneevent.h | 3 +- lib/events/simplestateevents.h | 5 +- lib/events/stateevent.h | 30 +-- lib/events/stickerevent.h | 3 +- 25 files changed, 326 insertions(+), 217 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 06e754aa..9983f860 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ endif(CMAKE_BUILD_TYPE) message(STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) include(CheckCXXCompilerFlag) if (MSVC) - add_compile_options(/EHsc /W4 + add_compile_options(/Zc:preprocessor /EHsc /W4 /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 4d539b85..70ca1c7e 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -9,7 +9,7 @@ namespace Quotient { class QUOTIENT_API CallAnswerEvent : public CallEventBase { public: - DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) + QUO_EVENT(CallAnswerEvent, "m.call.answer") explicit CallAnswerEvent(const QJsonObject& obj); @@ -20,5 +20,4 @@ public: return contentPart("answer"_ls).value("sdp"_ls).toString(); } }; -REGISTER_EVENT_TYPE(CallAnswerEvent) } // namespace Quotient diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index e949f722..cb96f358 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -11,7 +11,7 @@ namespace Quotient { class CallCandidatesEvent : public CallEventBase { public: - DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent) + QUO_EVENT(CallCandidatesEvent, "m.call.candidates") explicit CallCandidatesEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) @@ -27,6 +27,4 @@ public: QUO_CONTENT_GETTER(QString, callId) QUO_CONTENT_GETTER(int, version) }; - -REGISTER_EVENT_TYPE(CallCandidatesEvent) } // namespace Quotient diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index b0017c59..e4d9bb78 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -9,7 +9,7 @@ namespace Quotient { class QUOTIENT_API CallHangupEvent : public CallEventBase { public: - DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) + QUO_EVENT(CallHangupEvent, "m.call.hangup") explicit CallHangupEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) @@ -18,6 +18,4 @@ public: : CallEventBase(typeId(), matrixTypeId(), callId, 0) {} }; - -REGISTER_EVENT_TYPE(CallHangupEvent) } // namespace Quotient diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 5b4ca0df..f96f416d 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -9,7 +9,7 @@ namespace Quotient { class QUOTIENT_API CallInviteEvent : public CallEventBase { public: - DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) + QUO_EVENT(CallInviteEvent, "m.call.invite") explicit CallInviteEvent(const QJsonObject& obj); @@ -22,6 +22,4 @@ public: return contentPart("offer"_ls).value("sdp"_ls).toString(); } }; - -REGISTER_EVENT_TYPE(CallInviteEvent) } // namespace Quotient diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index 2018d3d6..942edba4 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -8,11 +8,10 @@ namespace Quotient { class QUOTIENT_API DirectChatEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) + QUO_EVENT(DirectChatEvent, "m.direct") explicit DirectChatEvent(const QJsonObject& obj) : Event(typeId(), obj) {} QMultiHash usersToDirectChats() const; }; -REGISTER_EVENT_TYPE(DirectChatEvent) } // namespace Quotient diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index ddd5e415..22e51cb8 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -27,7 +27,7 @@ namespace Quotient { */ class QUOTIENT_API EncryptedEvent : public RoomEvent { public: - DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) + QUO_EVENT(EncryptedEvent, "m.room.encrypted") /* In case with Olm, the encrypted content of the event is * a map from the recipient Curve25519 identity key to ciphertext @@ -59,6 +59,4 @@ public: void setRelation(const QJsonObject& relation); }; -REGISTER_EVENT_TYPE(EncryptedEvent) - } // namespace Quotient diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 91452c3f..60e77451 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -28,7 +28,7 @@ public: class QUOTIENT_API EncryptionEvent : public StateEvent { public: - DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) + QUO_EVENT(EncryptionEvent, "m.room.encryption") using EncryptionType [[deprecated("Use Quotient::EncryptionType instead")]] = @@ -48,5 +48,4 @@ public: bool useEncryption() const { return !algorithm().isEmpty(); } }; -REGISTER_EVENT_TYPE(EncryptionEvent) } // namespace Quotient diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 1f1eebaa..595e20a5 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -4,6 +4,7 @@ #include "event.h" #include "logging.h" +#include "stateevent.h" #include @@ -11,12 +12,38 @@ using namespace Quotient; QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId; } -void _impl::EventFactoryBase::logAddingMethod(event_type_t TypeId, - size_t newSize) +void AbstractEventMetaType::addDerived(AbstractEventMetaType* newType) { - qDebug(EVENTS) << "Adding factory method for" << TypeId << "events;" - << newSize << "methods will be in the" << name - << "chain"; + if (const auto existing = + std::find_if(derivedTypes.cbegin(), derivedTypes.cend(), + [&newType](const AbstractEventMetaType* t) { + return t->matrixId == newType->matrixId; + }); + existing != derivedTypes.cend()) + { + if (*existing == newType) + return; + // Two different metatype objects claim the same Matrix type id; this + // is not normal, so give as much information as possible to diagnose + if ((*existing)->className == newType->className) { + qCritical(EVENTS) + << newType->className << "claims" << newType->matrixId + << "repeatedly; check that it's exported across translation " + "units or shared objects"; + Q_ASSERT(false); // That situation is plain wrong + return; // So maybe std::terminate() even? + } + qWarning(EVENTS).nospace() + << newType->matrixId << " is already mapped to " + << (*existing)->className << " before " << newType->className + << "; unless the two have different isValid() conditions, the " + "latter class will never be used"; + } + derivedTypes.emplace_back(newType); + qDebug(EVENTS).nospace() + << newType->matrixId << " -> " << newType->className << "; " + << derivedTypes.size() << " derived type(s) registered for " + << className; } Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) @@ -48,6 +75,10 @@ const QJsonObject Event::unsignedJson() const return fullJson()[UnsignedKeyL].toObject(); } +bool Event::isStateEvent() const { return is(); } + +bool Event::isCallEvent() const { return is(); } + void Event::dumpTo(QDebug dbg) const { dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); diff --git a/lib/events/event.h b/lib/events/event.h index 711f8fd4..ea827244 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -85,91 +85,167 @@ inline event_ptr_tt makeEvent(ArgTs&&... args) return std::make_unique(std::forward(args)...); } -namespace _impl { - class QUOTIENT_API EventFactoryBase { - public: - EventFactoryBase(const EventFactoryBase&) = delete; - - protected: // This class is only to inherit from - explicit EventFactoryBase(const char* name) - : name(name) - {} - void logAddingMethod(event_type_t TypeId, size_t newSize); - - private: - const char* const name; - }; -} // namespace _impl - -//! \brief A family of event factories to create events from CS API responses -//! -//! Each of these factories, as instantiated by event base types (Event, -//! RoomEvent etc.) is capable of producing an event object derived from -//! \p BaseEventT, using the JSON payload and the event type passed to its -//! make() method. Don't use these directly to make events; use loadEvent() -//! overloads as the frontend for these. Never instantiate new factories -//! outside of base event classes. -//! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory, -//! StateEventBase::factory -template -class EventFactory : public _impl::EventFactoryBase { -private: - using method_t = event_ptr_tt (*)(const QJsonObject&, - const QString&); - std::vector methods {}; +// === EventMetaType === - template - static event_ptr_tt makeIfMatches(const QJsonObject& json, - const QString& matrixType) +class Event; + +template +bool is(const Event& e); + +//! \brief The base class for event metatypes +//! +//! You should not normally have to use this directly, unless you need to devise +//! a whole new kind of event metatypes. +class QUOTIENT_API AbstractEventMetaType { +public: + // The public fields here are const and are not to be changeable anyway. + // NOLINTBEGIN(misc-non-private-member-variables-in-classes) + const char* const className; + const event_type_t matrixId; + const AbstractEventMetaType* const baseType = nullptr; + // NOLINTEND(misc-non-private-member-variables-in-classes) + + explicit AbstractEventMetaType(const char* className) + : className(className) + {} + explicit AbstractEventMetaType(const char* className, event_type_t matrixId, + AbstractEventMetaType& nearestBase) + : className(className), matrixId(matrixId), baseType(&nearestBase) { - // If your matrix event type is not all ASCII, it's your problem - // (see https://github.com/matrix-org/matrix-doc/pull/2758) - return EventT::TypeId == matrixType ? makeEvent(json) : nullptr; + nearestBase.addDerived(this); } + void addDerived(AbstractEventMetaType *newType); + + virtual ~AbstractEventMetaType() = default; + +protected: + // Allow template specialisations to call into one another + template + friend class EventMetaType; + + // The returned value indicates whether a generic object has to be created + // on the top level when `event` is empty, instead of returning nullptr + virtual bool doLoadFrom(const QJsonObject& fullJson, const QString& type, + Event*& event) const = 0; + +private: + std::vector derivedTypes{}; + Q_DISABLE_COPY_MOVE(AbstractEventMetaType) +}; + +// Any event metatype is unique (note Q_DISABLE_COPY_MOVE above) so can be +// identified by its address +inline bool operator==(const AbstractEventMetaType& lhs, + const AbstractEventMetaType& rhs) +{ + return &lhs == &rhs; +} + +//! \brief A family of event meta-types to load and match events +//! +//! TL;DR for the loadFrom() story: +//! - for base event types, use QUO_BASE_EVENT and, if you have additional +//! validation (e.g., JSON has to contain a certain key - see StateEventBase +//! for a real example), define it in the static EventT::isValid() member +//! function accepting QJsonObject and returning bool. +//! - for leaf (specific) event types - simply use QUO_EVENT and it will do +//! everything necessary, including the TypeId definition. +//! \sa QUO_EVENT, QUO_BASE_EVENT +template +class QUOTIENT_API EventMetaType : public AbstractEventMetaType { + // Above: can't constrain EventT to be EventClass because it's incomplete + // at the point of EventMetaType instantiation. public: - explicit EventFactory(const char* fName) - : EventFactoryBase { fName } - {} + using AbstractEventMetaType::AbstractEventMetaType; - //! \brief Add a method to create events of a given type + //! \brief Try to load an event from JSON, with dynamic type resolution //! - //! Adds a standard factory method (makeIfMatches) for \p EventT so that - //! event objects of this type can be created dynamically by loadEvent. - //! The caller is responsible for ensuring this method is called only - //! once per type. - //! \sa loadEvent, Quotient::loadEvent - template - const auto& addMethod() + //! The generic logic defined in this class template and invoked applies to + //! all event types defined in the library and boils down to the following: + //! 1. + //! a. If EventT has TypeId defined (which normally is a case of + //! all leaf - specific - event types, via QUO_EVENT macro) and + //! \p type doesn't exactly match it, nullptr is immediately returned. + //! b. In absence of TypeId, an event type is assumed to be a base; + //! its derivedTypes are examined, and this algorithm is applied + //! recursively on each. + //! 2. Optional validation: if EventT (or, due to the way inheritance works, + //! any of its base event types) has a static isValid() predicate and + //! the event JSON does not satisfy it, nullptr is immediately returned + //! to the upper level or to the loadFrom() caller. This is how existence + //! of `state_key` is checked in any type derived from StateEventBase. + //! 3. If step 1b above returned non-nullptr, immediately return it. + //! 4. + //! a. If EventT::isValid() or EventT::TypeId (either, or both) exist and + //! are satisfied (see steps 1a and 2 above), an object of this type + //! is created from the passed JSON and returned. In case of a base + //! event type, this will be a generic (aka "unknown") event. + //! b. If neither exists, a generic event is only created and returned + //! when on the top level (i.e., outside of recursion into + //! derivedTypes); lower levels return nullptr instead and the type + //! lookup continues. The latter is a case of a derived base event + //! metatype (e.g. RoomEvent) called from its base event metatype + //! (i.e., Event). If no matching type derived from RoomEvent is found, + //! the nested lookup returns nullptr rather than a generic RoomEvent, + //! so that other types derived from Event could be examined. + event_ptr_tt loadFrom(const QJsonObject& fullJson, + const QString& type) const { - const auto m = &makeIfMatches; - const auto it = std::find(methods.cbegin(), methods.cend(), m); - if (it != methods.cend()) - return *it; - logAddingMethod(EventT::TypeId, methods.size() + 1); - return methods.emplace_back(m); + Event* event = nullptr; + const bool goodEnough = doLoadFrom(fullJson, type, event); + if (!event && goodEnough) + return event_ptr_tt{ makeEvent(fullJson) }; + return event_ptr_tt{ static_cast(event) }; } - auto loadEvent(const QJsonObject& json, const QString& matrixType) +private: + bool doLoadFrom(const QJsonObject& fullJson, const QString& type, + Event*& event) const override + { + if constexpr (requires { EventT::TypeId; }) { + if (EventT::TypeId != type) + return false; + } else { + for (const auto& p : derivedTypes) { + p->doLoadFrom(fullJson, type, event); + if (event) { + Q_ASSERT(is(*event)); + return false; + } + } + } + if constexpr (requires { EventT::isValid; }) { + if (!EventT::isValid(fullJson)) + return false; + } else if constexpr (!requires { EventT::TypeId; }) + return true; // Create a generic event object if on the top level + event = makeEvent(fullJson); + return false; + } + static auto makeEvent(const QJsonObject& fullJson) { - for (const auto& f : methods) - if (auto e = f(json, matrixType)) - return e; - return makeEvent(UnknownEventTypeId, json); + if constexpr (requires { EventT::TypeId; }) + return new EventT(fullJson); + else + return new EventT(UnknownEventTypeId, fullJson); } }; -//! \brief Point of customisation to dynamically load events -//! -//! The default specialisation of this calls BaseEventT::factory.loadEvent() -//! and if that fails (i.e. returns nullptr) creates an unknown event of -//! BaseEventT. Other specialisations may reuse other factories, add validations -//! common to BaseEventT events, and so on. -template -event_ptr_tt doLoadEvent(const QJsonObject& json, - const QString& matrixType) +template +constexpr const auto& mostSpecificMetaType() { - return BaseEventT::factory.loadEvent(json, matrixType); + if constexpr (requires { EventT::MetaType; }) + return EventT::MetaType; + else + return EventT::BaseMetaType; +} + +template +inline event_ptr_tt doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + return mostSpecificMetaType().loadFrom(json, matrixType); } // === Event === @@ -177,7 +253,12 @@ event_ptr_tt doLoadEvent(const QJsonObject& json, class QUOTIENT_API Event { public: using Type = event_type_t; - static inline EventFactory factory { "Event" }; + static inline EventMetaType BaseMetaType { "Event" }; + virtual const AbstractEventMetaType& metaType() const + { + return BaseMetaType; + } + explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, @@ -194,8 +275,26 @@ public: return { { TypeKey, matrixType }, { ContentKey, content } }; } - Type type() const { return _type; } + //! \brief Event Matrix type, as identified by its metatype object + //! + //! For generic/unknown events it will contain a descriptive/generic string + //! defined by the respective base event type (that can be empty). + //! \sa matrixType + Type type() const { return metaType().matrixId; } + + //! \brief Exact Matrix type stored in JSON + //! + //! Coincides with the result of type() (but is slower) for events defined + //! in C++ (not necessarily in the library); for generic/unknown events + //! the returned value will be different. QString matrixType() const; + + template + bool is() const + { + return Quotient::is(*this); + } + [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() " "or by other means")]] QByteArray originalJson() const; @@ -237,13 +336,17 @@ public: friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; - dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): "; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.metaType().className << "): "; e.dumpTo(dbg); return dbg; } - virtual bool isStateEvent() const { return false; } - virtual bool isCallEvent() const { return false; } + // State events are quite special in Matrix; so isStateEvent() is here, + // as an exception. For other base events, Event::is<>() and + // Quotient::is<>() should be used; don't add is* methods here + bool isStateEvent() const; + [[deprecated("Use is() instead")]] bool isCallEvent() const; protected: QJsonObject& editJson() { return _json; } @@ -259,11 +362,64 @@ template using EventsArray = std::vector>; using Events = EventsArray; +// === Facilities for event class definitions === + +//! \brief Supply event metatype information in base event types +//! +//! Use this macro in a public section of your base event class to provide +//! type identity and enable dynamic loading of generic events of that type. +//! Do _not_ add this macro if your class is an intermediate wrapper and is not +//! supposed to be instantiated on its own. Provides BaseMetaType static field +//! initialised by parameters passed to the macro, and a metaType() override +//! pointing to that BaseMetaType. +//! \sa EventMetaType, EventMetaType::SuppressLoadDerived +#define QUO_BASE_EVENT(CppType_, ...) \ + static inline EventMetaType BaseMetaType{ \ + #CppType_ __VA_OPT__(,) __VA_ARGS__ }; \ + const AbstractEventMetaType& metaType() const override \ + { \ + return BaseMetaType; \ + } \ + // End of macro + +//! Supply event metatype information in (specific) event types +//! +//! Use this macro in a public section of your event class to provide type +//! identity and enable dynamic loading of generic events of that type. +//! Do _not_ use this macro if your class is an intermediate wrapper and is not +//! supposed to be instantiated on its own. Provides MetaType static field +//! initialised as described below; a metaType() override pointing to it; and +//! the TypeId static field that is equal to MetaType.matrixId. +//! +//! The first two macro parameters are used as the first two EventMetaType +//! constructor parameters; the third EventMetaType parameter is always +//! BaseMetaType; and additional base types can be passed in extra macro +//! parameters if you need to include the same event type in more than one +//! event factory hierarchy (e.g., EncryptedEvent). +//! \sa EventMetaType +#define QUO_EVENT(CppType_, MatrixType_, ...) \ + static inline const auto& TypeId = MatrixType_##_ls; \ + static inline const EventMetaType MetaType{ \ + #CppType_, TypeId, BaseMetaType __VA_OPT__(,) __VA_ARGS__ \ + }; \ + const AbstractEventMetaType& metaType() const override \ + { \ + return MetaType; \ + } \ + [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \ + static constexpr const char* matrixTypeId() { return MatrixType_; } \ + [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \ + static event_type_t typeId() { return TypeId; } \ + // End of macro + +//! \deprecated This is the old name for what is now known as QUO_EVENT +#define DEFINE_EVENT_TYPEID(Type_, Id_) QUO_EVENT(Type_, Id_) + #define QUO_CONTENT_GETTER_X(PartType_, PartName_, JsonKey_) \ - PartType_ PartName_() const \ - { \ - static const auto PartName_##JsonKey = JsonKey_; \ - return contentPart(PartName_##JsonKey); \ + PartType_ PartName_() const \ + { \ + static const auto PartName_##JsonKey = JsonKey_; \ + return contentPart(PartName_##JsonKey); \ } //! \brief Define an inline method obtaining a content part @@ -277,25 +433,9 @@ using Events = EventsArray; #define QUO_CONTENT_GETTER(PartType_, PartName_) \ QUO_CONTENT_GETTER_X(PartType_, PartName_, toSnakeCase(#PartName_##_ls)) -// === Facilities for event class definitions === - -// This macro should be used in a public section of an event class to -// provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(Id_, Type_) \ - static constexpr event_type_t TypeId = Id_##_ls; \ - [[deprecated("Use " #Type_ "::TypeId directly instead")]] \ - static constexpr event_mtype_t matrixTypeId() { return Id_; } \ - [[deprecated("Use " #Type_ "::TypeId directly instead")]] \ - static event_type_t typeId() { return TypeId; } \ - // End of macro - -// This macro should be put after an event class definition (in .h or .cpp) -// to enable its deserialisation from a /sync and other -// polymorphic event arrays -#define REGISTER_EVENT_TYPE(Type_) \ - [[maybe_unused]] inline const auto& factoryMethodFor##Type_ = \ - Type_::factory.addMethod(); \ - // End of macro +//! \deprecated This macro was used after an event class definition +//! to enable its dynamic loading; it is completely superseded by QUO_EVENT +#define REGISTER_EVENT_TYPE(Type_) /// \brief Define a new event class with a single key-value pair in the content /// @@ -309,7 +449,7 @@ using Events = EventsArray; JsonKey_) \ class QUOTIENT_API Name_ : public Base_ { \ public: \ - DEFINE_EVENT_TYPEID(TypeId_, Name_) \ + QUO_EVENT(Name_, TypeId_) \ using value_type = ValueType_; \ explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \ explicit Name_(const value_type& v) \ @@ -318,7 +458,6 @@ using Events = EventsArray; QUO_CONTENT_GETTER_X(ValueType_, GetterName_, JsonKey) \ static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \ }; \ - REGISTER_EVENT_TYPE(Name_) \ // End of macro // === is<>(), eventCast<>() and switchOnType<>() === @@ -326,12 +465,16 @@ using Events = EventsArray; template inline bool is(const Event& e) { - return e.type() == typeId(); -} - -inline bool isUnknown(const Event& e) -{ - return e.type() == UnknownEventTypeId; + if constexpr (requires { EventT::MetaType; }) { + return &e.metaType() == &EventT::MetaType; + } else { + const auto* p = &e.metaType(); + do { + if (p == &EventT::BaseMetaType) + return true; + } while ((p = p->baseType) != nullptr); + return false; + } } //! \brief Cast the event pointer down in a type-safe way diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 5b587522..5b5a518f 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -13,7 +13,7 @@ static constexpr auto SasV1Method = "m.sas.v1"_ls; /// Typically sent as a to-device event. class QUOTIENT_API KeyVerificationRequestEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent) + QUO_EVENT(KeyVerificationRequestEvent, "m.key.verification.request") explicit KeyVerificationRequestEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -45,11 +45,10 @@ public: /// by the receiver. QUO_CONTENT_GETTER(QDateTime, timestamp) }; -REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) class QUOTIENT_API KeyVerificationReadyEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.ready", KeyVerificationReadyEvent) + QUO_EVENT(KeyVerificationReadyEvent, "m.key.verification.ready") explicit KeyVerificationReadyEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -72,13 +71,11 @@ public: /// The verification methods supported by the sender. QUO_CONTENT_GETTER(QStringList, methods) }; -REGISTER_EVENT_TYPE(KeyVerificationReadyEvent) - /// Begins a key verification process. class QUOTIENT_API KeyVerificationStartEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) + QUO_EVENT(KeyVerificationStartEvent, "m.key.verification.start") explicit KeyVerificationStartEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -146,13 +143,12 @@ public: return contentPart("short_authentification_string"_ls); } }; -REGISTER_EVENT_TYPE(KeyVerificationStartEvent) /// Accepts a previously sent m.key.verification.start message. /// Typically sent as a to-device event. class QUOTIENT_API KeyVerificationAcceptEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent) + QUO_EVENT(KeyVerificationAcceptEvent, "m.key.verification.accept") explicit KeyVerificationAcceptEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -199,11 +195,10 @@ public: /// canonical JSON representation of the m.key.verification.start message. QUO_CONTENT_GETTER(QString, commitment) }; -REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) class QUOTIENT_API KeyVerificationCancelEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) + QUO_EVENT(KeyVerificationCancelEvent, "m.key.verification.cancel") explicit KeyVerificationCancelEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -228,13 +223,12 @@ public: /// The error code for why the process/request was cancelled by the user. QUO_CONTENT_GETTER(QString, code) }; -REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) + QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key") explicit KeyVerificationKeyEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -251,12 +245,11 @@ public: /// The device's ephemeral public key, encoded as unpadded base64. QUO_CONTENT_GETTER(QString, key) }; -REGISTER_EVENT_TYPE(KeyVerificationKeyEvent) /// Sends the MAC of a device's key to the partner device. class QUOTIENT_API KeyVerificationMacEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) + QUO_EVENT(KeyVerificationMacEvent, "m.key.verification.mac") explicit KeyVerificationMacEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -280,11 +273,10 @@ public: return contentPart>("mac"_ls); } }; -REGISTER_EVENT_TYPE(KeyVerificationMacEvent) class QUOTIENT_API KeyVerificationDoneEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationDoneEvent) + QUO_EVENT(KeyVerificationDoneEvent, "m.key.verification.done") explicit KeyVerificationDoneEvent(const QJsonObject& obj) : Event(TypeId, obj) @@ -297,6 +289,4 @@ public: /// The same transactionId as before QUO_CONTENT_GETTER(QString, transactionId) }; -REGISTER_EVENT_TYPE(KeyVerificationDoneEvent) - } // namespace Quotient diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index 5e077e47..a02f4592 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -21,11 +21,10 @@ using EventsWithReceipts = QVector; class QUOTIENT_API ReceiptEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) + QUO_EVENT(ReceiptEvent, "m.receipt") explicit ReceiptEvent(const EventsWithReceipts& ewrs); explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {} EventsWithReceipts eventsWithReceipts() const; }; -REGISTER_EVENT_TYPE(ReceiptEvent) } // namespace Quotient diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index 63617e54..c193054a 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -8,7 +8,7 @@ namespace Quotient { class QUOTIENT_API RedactionEvent : public RoomEvent { public: - DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) + QUO_EVENT(RedactionEvent, "m.room.redaction") explicit RedactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} @@ -19,5 +19,4 @@ public: } QUO_CONTENT_GETTER(QString, reason) }; -REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index af291696..2ebe29bf 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -14,7 +14,7 @@ class QUOTIENT_API RoomAvatarEvent // without a thumbnail. But The Spec says there be thumbnails, and // we follow The Spec. public: - DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent) + QUO_EVENT(RoomAvatarEvent, "m.room.avatar") explicit RoomAvatarEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} explicit RoomAvatarEvent(const EventContent::ImageContent& avatar) @@ -31,5 +31,4 @@ public: QUrl url() const { return content().url(); } }; -REGISTER_EVENT_TYPE(RoomAvatarEvent) } // namespace Quotient diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index 60ca68ac..e1c7888e 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -34,7 +34,7 @@ inline auto toJson(const EventContent::AliasesEventContent& c) class QUOTIENT_API RoomCanonicalAliasEvent : public StateEvent { public: - DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent) + QUO_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias") explicit RoomCanonicalAliasEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) @@ -55,5 +55,4 @@ public: QString alias() const { return content().canonicalAlias; } QStringList altAliases() const { return content().altAliases; } }; -REGISTER_EVENT_TYPE(RoomCanonicalAliasEvent) } // namespace Quotient diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 989030ac..f22752b4 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -9,7 +9,7 @@ namespace Quotient { class QUOTIENT_API RoomCreateEvent : public StateEventBase { public: - DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + QUO_EVENT(RoomCreateEvent, "m.room.create") explicit RoomCreateEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) @@ -26,5 +26,4 @@ public: bool isUpgrade() const; RoomType roomType() const; }; -REGISTER_EVENT_TYPE(RoomCreateEvent) } // namespace Quotient diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 9461340b..532e72e2 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -10,10 +10,11 @@ namespace Quotient { class RedactionEvent; -/** This class corresponds to m.room.* events */ +// That check could look into Event and find most stuff already deleted... +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) class QUOTIENT_API RoomEvent : public Event { public: - static inline EventFactory factory { "RoomEvent" }; + QUO_BASE_EVENT(RoomEvent, {}, Event::BaseMetaType) // RedactionEvent is an incomplete type here so we cannot inline // constructors and destructors and we cannot use 'using'. @@ -80,21 +81,14 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = Range; -template <> -inline EventPtr doLoadEvent(const QJsonObject& json, const QString& matrixType) -{ - if (matrixType == "m.room.encrypted") - return RoomEvent::factory.loadEvent(json, matrixType); - return Event::factory.loadEvent(json, matrixType); -} - class QUOTIENT_API CallEventBase : public RoomEvent { public: + QUO_BASE_EVENT(CallEventBase, "m.call.*"_ls, RoomEvent::BaseMetaType) + CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson = {}); CallEventBase(Type type, const QJsonObject& json); ~CallEventBase() override = default; - bool isCallEvent() const override { return true; } QUO_CONTENT_GETTER(QString, callId) QUO_CONTENT_GETTER(int, version) diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 0dfdf383..6883a2a5 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -9,7 +9,7 @@ namespace Quotient { class QUOTIENT_API RoomKeyEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) + QUO_EVENT(RoomKeyEvent, "m.room_key") explicit RoomKeyEvent(const QJsonObject& obj); explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, @@ -23,5 +23,4 @@ public: return contentPart("session_key"_ls).toLatin1(); } }; -REGISTER_EVENT_TYPE(RoomKeyEvent) } // namespace Quotient diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index dd33ea6b..c690586e 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -31,7 +31,7 @@ using MembershipType [[deprecated("Use Membership instead")]] = Membership; class QUOTIENT_API RoomMemberEvent : public StateEvent { Q_GADGET public: - DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) + QUO_EVENT(RoomMemberEvent, "m.room.member") using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; @@ -79,14 +79,4 @@ public: bool isRename() const; bool isAvatarUpdate() const; }; - -template <> -inline event_ptr_tt -doLoadEvent(const QJsonObject& json, const QString& matrixType) -{ - if (matrixType == QLatin1String(RoomMemberEvent::matrixTypeId())) - return makeEvent(json); - return makeEvent(unknownEventTypeId(), json); -} -REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 6968ad70..889fc4dc 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -20,7 +20,7 @@ namespace MessageEventContent = EventContent; // Back-compatibility class QUOTIENT_API RoomMessageEvent : public RoomEvent { Q_GADGET public: - DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) + QUO_EVENT(RoomMessageEvent, "m.room.message") enum class MsgType { Text, @@ -94,7 +94,7 @@ private: Q_ENUM(MsgType) }; -REGISTER_EVENT_TYPE(RoomMessageEvent) + using MessageEventType = RoomMessageEvent::MsgType; namespace EventContent { diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index a1638a27..7ac12db0 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -33,7 +33,7 @@ struct QUOTIENT_API PowerLevelsEventContent { class QUOTIENT_API RoomPowerLevelsEvent : public StateEvent { public: - DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent) + QUO_EVENT(RoomPowerLevelsEvent, "m.room.power_levels") explicit RoomPowerLevelsEvent(PowerLevelsEventContent&& content) : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) @@ -61,5 +61,4 @@ public: int powerLevelForState(const QString& eventId) const; int powerLevelForUser(const QString& userId) const; }; -REGISTER_EVENT_TYPE(RoomPowerLevelsEvent) } // namespace Quotient diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 15d26923..97586587 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -8,7 +8,7 @@ namespace Quotient { class QUOTIENT_API RoomTombstoneEvent : public StateEventBase { public: - DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) + QUO_EVENT(RoomTombstoneEvent, "m.room.tombstone") explicit RoomTombstoneEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) @@ -17,5 +17,4 @@ public: QString serverMessage() const; QString successorRoomId() const; }; -REGISTER_EVENT_TYPE(RoomTombstoneEvent) } // namespace Quotient diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index a8eaab56..c79d03b0 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -14,7 +14,7 @@ namespace Quotient { EventContent::SingleKeyValue<_ValueType, &_Name##Key>> { \ public: \ using value_type = _ValueType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ + QUO_EVENT(_Name, _TypeId) \ template \ explicit _Name(T&& value) \ : StateEvent(TypeId, matrixTypeId(), QString(), \ @@ -25,7 +25,6 @@ namespace Quotient { {} \ auto _ContentKey() const { return content().value; } \ }; \ - REGISTER_EVENT_TYPE(_Name) \ // End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) @@ -38,7 +37,7 @@ class QUOTIENT_API RoomAliasesEvent : public StateEvent< EventContent::SingleKeyValue> { public: - DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) + QUO_EVENT(RoomAliasesEvent, "m.room.aliases") explicit RoomAliasesEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 9f1d7118..74876803 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -9,7 +9,12 @@ namespace Quotient { class QUOTIENT_API StateEventBase : public RoomEvent { public: - static inline EventFactory factory { "StateEvent" }; + QUO_BASE_EVENT(StateEventBase, "json.contains('state_key')"_ls, + RoomEvent::BaseMetaType) + static bool isValid(const QJsonObject& fullJson) + { + return fullJson.contains(StateKeyKeyL); + } StateEventBase(Type type, const QJsonObject& json); StateEventBase(Type type, event_mtype_t matrixType, @@ -27,7 +32,6 @@ public: { ContentKey, contentJson } }; } - bool isStateEvent() const override { return true; } QString replacedState() const; void dumpTo(QDebug dbg) const override; @@ -44,28 +48,6 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, return StateEventBase::basicJson(matrixTypeId, stateKey, content); } -//! \brief Override RoomEvent factory with that from StateEventBase if JSON has -//! stateKey -//! -//! This means in particular that an event with a type known to RoomEvent but -//! having stateKey set (even to an empty value) will be treated as a state -//! event and most likely end up as unknown (consider, e.g., m.room.message -//! that has stateKey set). -template <> -inline RoomEventPtr doLoadEvent(const QJsonObject& json, - const QString& matrixType) -{ - if (json.contains(StateKeyKeyL)) - return StateEventBase::factory.loadEvent(json, matrixType); - return RoomEvent::factory.loadEvent(json, matrixType); -} - -template <> -inline bool is(const Event& e) -{ - return e.isStateEvent(); -} - /** * A combination of event type and state key uniquely identifies a piece * of state in Matrix. diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h index e378422d..67905481 100644 --- a/lib/events/stickerevent.h +++ b/lib/events/stickerevent.h @@ -14,7 +14,7 @@ namespace Quotient { class QUOTIENT_API StickerEvent : public RoomEvent { public: - DEFINE_EVENT_TYPEID("m.sticker", StickerEvent) + QUO_EVENT(StickerEvent, "m.sticker") explicit StickerEvent(const QJsonObject& obj) : RoomEvent(TypeId, obj) @@ -45,5 +45,4 @@ public: private: EventContent::ImageContent m_imageContent; }; -REGISTER_EVENT_TYPE(StickerEvent) } // namespace Quotient -- 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. --- CMakeLists.txt | 2 +- lib/connection.cpp | 1 - lib/events/callanswerevent.cpp | 15 +-- lib/events/callanswerevent.h | 5 +- lib/events/callcandidatesevent.h | 9 +- lib/events/callhangupevent.h | 12 +-- lib/events/callinviteevent.cpp | 9 +- lib/events/callinviteevent.h | 5 +- lib/events/directchatevent.h | 2 +- lib/events/encryptedevent.cpp | 22 ++--- lib/events/encryptionevent.h | 10 +- lib/events/event.cpp | 7 +- lib/events/event.h | 174 +++++++++++++++++++++++------------ lib/events/eventloader.h | 40 +------- lib/events/keyverificationevent.h | 32 ++----- lib/events/receiptevent.cpp | 16 ++-- lib/events/receiptevent.h | 13 ++- lib/events/redactionevent.h | 3 +- lib/events/roomavatarevent.h | 19 +--- lib/events/roomcanonicalaliasevent.h | 20 +--- lib/events/roomcreateevent.h | 4 +- lib/events/roomevent.cpp | 17 +--- lib/events/roomevent.h | 22 +++-- lib/events/roomkeyevent.cpp | 22 ----- lib/events/roomkeyevent.h | 11 ++- lib/events/roommemberevent.h | 22 +---- lib/events/roommessageevent.cpp | 7 +- lib/events/roompowerlevelsevent.h | 9 +- lib/events/roomtombstoneevent.h | 4 +- lib/events/simplestateevents.h | 42 ++++----- lib/events/stateevent.cpp | 14 ++- lib/events/stateevent.h | 115 ++++++++++++++--------- lib/room.cpp | 2 +- lib/roomstateview.h | 114 ++++++++++++++++++----- lib/syncdata.cpp | 2 - 35 files changed, 405 insertions(+), 418 deletions(-) delete mode 100644 lib/events/roomkeyevent.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9983f860..d4cf52d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,7 +165,7 @@ list(APPEND lib_SRCS lib/events/directchatevent.h lib/events/directchatevent.cpp lib/events/encryptionevent.h lib/events/encryptionevent.cpp lib/events/encryptedevent.h lib/events/encryptedevent.cpp - lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp + lib/events/roomkeyevent.h lib/events/stickerevent.h lib/events/filesourceinfo.h lib/events/filesourceinfo.cpp lib/jobs/requestdata.h lib/jobs/requestdata.cpp diff --git a/lib/connection.cpp b/lib/connection.cpp index 722829e8..471dc20d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -28,7 +28,6 @@ #include "csapi/whoami.h" #include "events/directchatevent.h" -#include "events/eventloader.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index f75f8ad3..89dcd7fd 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -26,16 +26,9 @@ m.call.answer using namespace Quotient; -CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Answer event"; -} - CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) - : CallEventBase( - typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("answer"), - QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } }) + : EventTemplate(callId, { { QStringLiteral("answer"), + QJsonObject { { QStringLiteral("type"), + QStringLiteral("answer") }, + { QStringLiteral("sdp"), sdp } } } }) {} diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 70ca1c7e..c5ad14df 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -7,11 +7,12 @@ #include "roomevent.h" namespace Quotient { -class QUOTIENT_API CallAnswerEvent : public CallEventBase { +class QUOTIENT_API CallAnswerEvent + : public EventTemplate { public: QUO_EVENT(CallAnswerEvent, "m.call.answer") - explicit CallAnswerEvent(const QJsonObject& obj); + using EventTemplate::EventTemplate; explicit CallAnswerEvent(const QString& callId, const QString& sdp); diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index cb96f358..f5d2f815 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -9,18 +9,15 @@ #include "roomevent.h" namespace Quotient { -class CallCandidatesEvent : public CallEventBase { +class CallCandidatesEvent : public EventTemplate { public: QUO_EVENT(CallCandidatesEvent, "m.call.candidates") - explicit CallCandidatesEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) - {} + using EventTemplate::EventTemplate; explicit CallCandidatesEvent(const QString& callId, const QJsonArray& candidates) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("candidates"), candidates } }) + : EventTemplate(callId, { { QStringLiteral("candidates"), candidates } }) {} QUO_CONTENT_GETTER(QJsonArray, candidates) diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index e4d9bb78..f0b131b9 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -7,15 +7,11 @@ #include "roomevent.h" namespace Quotient { -class QUOTIENT_API CallHangupEvent : public CallEventBase { +class QUOTIENT_API CallHangupEvent + : public EventTemplate { public: QUO_EVENT(CallHangupEvent, "m.call.hangup") - - explicit CallHangupEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) - {} - explicit CallHangupEvent(const QString& callId) - : CallEventBase(typeId(), matrixTypeId(), callId, 0) - {} + using EventTemplate::EventTemplate; }; +//REGISTER_EVENT_TYPE(CallHangupEvent) } // namespace Quotient diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 2f26a1cb..0232275b 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -27,16 +27,9 @@ m.call.invite using namespace Quotient; -CallInviteEvent::CallInviteEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Invite event"; -} - CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, const QString& sdp) - : CallEventBase( - typeId(), matrixTypeId(), callId, 0, + : EventTemplate(callId, { { QStringLiteral("lifetime"), lifetime }, { QStringLiteral("offer"), QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") }, diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index f96f416d..fc22f7e1 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -7,11 +7,12 @@ #include "roomevent.h" namespace Quotient { -class QUOTIENT_API CallInviteEvent : public CallEventBase { +class QUOTIENT_API CallInviteEvent + : public EventTemplate { public: QUO_EVENT(CallInviteEvent, "m.call.invite") - explicit CallInviteEvent(const QJsonObject& obj); + using EventTemplate::EventTemplate; explicit CallInviteEvent(const QString& callId, int lifetime, const QString& sdp); diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index 942edba4..0756d816 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -10,7 +10,7 @@ class QUOTIENT_API DirectChatEvent : public Event { public: QUO_EVENT(DirectChatEvent, "m.direct") - explicit DirectChatEvent(const QJsonObject& obj) : Event(typeId(), obj) {} + using Event::Event; QMultiHash usersToDirectChats() const; }; diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index ec00ad4c..c539d5b2 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,33 +2,29 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" -#include "roommessageevent.h" -#include "events/eventloader.h" using namespace Quotient; EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext, const QString& senderKey) - : RoomEvent(typeId(), matrixTypeId(), - { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, + : RoomEvent({ { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, { CiphertextKeyL, ciphertext }, { SenderKeyKeyL, senderKey } }) {} EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId) - : RoomEvent(typeId(), matrixTypeId(), - { - { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, - { CiphertextKeyL, QString(ciphertext) }, - { DeviceIdKeyL, deviceId }, - { SenderKeyKeyL, senderKey }, - { SessionIdKeyL, sessionId }, - }) + : RoomEvent({ + { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL, QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId }, + }) {} EncryptedEvent::EncryptedEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) + : RoomEvent(obj) { qCDebug(E2EE) << "Encrypted event from" << senderId(); } diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 60e77451..4bf7459c 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -26,7 +26,8 @@ public: int rotationPeriodMsgs = 100; }; -class QUOTIENT_API EncryptionEvent : public StateEvent { +class QUOTIENT_API EncryptionEvent + : public KeylessStateEventBase { public: QUO_EVENT(EncryptionEvent, "m.room.encryption") @@ -34,12 +35,7 @@ public: [[deprecated("Use Quotient::EncryptionType instead")]] = Quotient::EncryptionType; - explicit EncryptionEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - {} - explicit EncryptionEvent(EncryptionEventContent&& content) - : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) - {} + using KeylessStateEventBase::KeylessStateEventBase; Quotient::EncryptionType encryption() const { return content().encryption; } QString algorithm() const { return content().algorithm; } diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 595e20a5..2843e1dc 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -46,7 +46,8 @@ void AbstractEventMetaType::addDerived(AbstractEventMetaType* newType) << className; } -Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) +Event::Event(const QJsonObject& json) + : _json(json) { if (!json.contains(ContentKeyL) && !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) { @@ -55,10 +56,6 @@ Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) } } -Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) - : Event(type, basicJson(matrixType, contentJson)) -{} - Event::~Event() = default; QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } diff --git a/lib/events/event.h b/lib/events/event.h index ea827244..8a8d64b0 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -9,7 +9,7 @@ #include "single_key_value.h" namespace Quotient { -// === event_ptr_tt<> and type casting facilities === +// === event_ptr_tt<> and basic type casting facilities === template using event_ptr_tt = std::unique_ptr; @@ -49,42 +49,18 @@ const QString RoomIdKey { RoomIdKeyL }; const QString UnsignedKey { UnsignedKeyL }; const QString StateKeyKey { StateKeyKeyL }; -// === Event types === - using event_type_t = QLatin1String; -using event_mtype_t = const char*; - -class QUOTIENT_API EventTypeRegistry { -public: - ~EventTypeRegistry() = default; - [[deprecated("event_type_t is a string now, use it directly instead")]] +// TODO: Remove in 0.8 +struct QUOTIENT_API EventTypeRegistry { + [[deprecated("event_type_t is a string since libQuotient 0.7, use it directly instead")]] static QString getMatrixType(event_type_t typeId); -private: - EventTypeRegistry() = default; + EventTypeRegistry() = delete; + ~EventTypeRegistry() = default; Q_DISABLE_COPY_MOVE(EventTypeRegistry) }; -template -constexpr event_type_t typeId() -{ - return std::decay_t::TypeId; -} - -constexpr event_type_t UnknownEventTypeId = "?"_ls; -[[deprecated("Use UnknownEventTypeId")]] -constexpr event_type_t unknownEventTypeId() { return UnknownEventTypeId; } - -// === Event creation facilities === - -//! Create an event of arbitrary type from its arguments -template -inline event_ptr_tt makeEvent(ArgTs&&... args) -{ - return std::make_unique(std::forward(args)...); -} - // === EventMetaType === class Event; @@ -195,7 +171,7 @@ public: Event* event = nullptr; const bool goodEnough = doLoadFrom(fullJson, type, event); if (!event && goodEnough) - return event_ptr_tt{ makeEvent(fullJson) }; + return event_ptr_tt{ new EventT(fullJson) }; return event_ptr_tt{ static_cast(event) }; } @@ -220,18 +196,20 @@ private: return false; } else if constexpr (!requires { EventT::TypeId; }) return true; // Create a generic event object if on the top level - event = makeEvent(fullJson); + event = new EventT(fullJson); return false; } - static auto makeEvent(const QJsonObject& fullJson) - { - if constexpr (requires { EventT::TypeId; }) - return new EventT(fullJson); - else - return new EventT(UnknownEventTypeId, fullJson); - } }; +// === Event creation facilities === + +//! \brief Create an event of arbitrary type from its arguments +template +inline event_ptr_tt makeEvent(ArgTs&&... args) +{ + return std::make_unique(std::forward(args)...); +} + template constexpr const auto& mostSpecificMetaType() { @@ -241,13 +219,43 @@ constexpr const auto& mostSpecificMetaType() return EventT::BaseMetaType; } -template -inline event_ptr_tt doLoadEvent(const QJsonObject& json, - const QString& matrixType) +//! \brief Create an event with proper type from a JSON object +//! +//! Use this factory template to detect the type from the JSON object +//! contents (the detected event type should derive from the template +//! parameter type) and create an event object of that type. +template +inline event_ptr_tt loadEvent(const QJsonObject& fullJson) { - return mostSpecificMetaType().loadFrom(json, matrixType); + return mostSpecificMetaType().loadFrom( + fullJson, fullJson[TypeKeyL].toString()); } +//! \brief Create an event from a type string and content JSON +//! +//! Use this template to resolve the C++ type from the Matrix type string in +//! \p matrixType and create an event of that type by passing all parameters +//! to BaseEventT::basicJson(). +template +inline event_ptr_tt loadEvent(const QString& matrixType, + const auto&... otherBasicJsonParams) +{ + return mostSpecificMetaType().loadFrom( + EventT::basicJson(matrixType, otherBasicJsonParams...), matrixType); +} + +template +struct JsonConverter> + : JsonObjectUnpacker> { + // No dump() to avoid any ambiguity on whether a given export to JSON uses + // fullJson() or only contentJson() + using JsonObjectUnpacker>::load; + static auto load(const QJsonObject& jo) + { + return loadEvent(jo); + } +}; + // === Event === class QUOTIENT_API Event { @@ -259,10 +267,8 @@ public: return BaseMetaType; } + explicit Event(const QJsonObject& json); - explicit Event(Type type, const QJsonObject& json); - explicit Event(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); Q_DISABLE_COPY(Event) Event(Event&&) = default; Event& operator=(Event&&) = delete; @@ -312,6 +318,11 @@ public: const QJsonObject contentJson() const; + //! \brief Get a part of the content object, assuming a given type + //! + //! This retrieves the value under `content.` from the event JSON and + //! then converts it to \p T using fromJson(). + //! \sa contentJson, fromJson template const T contentPart(KeyT&& key) const { @@ -327,6 +338,11 @@ public: const QJsonObject unsignedJson() const; + //! \brief Get a part of the unsigned object, assuming a given type + //! + //! This retrieves the value under `unsigned.` from the event JSON and + //! then converts it to \p T using fromJson(). + //! \sa unsignedJson, fromJson template const T unsignedPart(KeyT&& key) const { @@ -353,7 +369,6 @@ protected: virtual void dumpTo(QDebug dbg) const; private: - Type _type; QJsonObject _json; }; using EventPtr = event_ptr_tt; @@ -364,6 +379,45 @@ using Events = EventsArray; // === Facilities for event class definitions === +//! \brief A template base class to derive your event type from +//! +//! This simple class template generates commonly used event constructor +//! signatures and the content() method with the appropriate return type. +//! The generic version here is only used with non-trivial \p ContentT (if you +//! don't need to create an event from its content structure, just go and derive +//! straight from the respective \p EventBaseT instead of using EventTemplate); +//! specialisations may override that and provide useful semantics even without +//! \p ContentT (see EventTemplate, e.g.). +//! +//! The template uses CRTP to pick the event type id from the actual class; +//! it will fail to compile if \p EventT doesn't provide TypeId. It also uses +//! the base event type's basicJson(); if you need extra keys to be inserted +//! you may want to bypass this template as writing the code to that effect in +//! your class will likely be clearer and more concise. +//! \sa https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern +//! \sa DEFINE_SIMPLE_EVENT +template +class EventTemplate : public BaseEventT { +public: + static_assert( + !std::is_same_v, + "If you see this, you tried to use EventTemplate with the default" + " ContentT type, which is void. This default is only used with explicit" + " specialisations (see CallEventBase, e.g.). Otherwise, if you don't" + " intend to use the content part of EventTemplate then you don't need" + " EventTemplate; just use the base event class directly"); + using content_type = ContentT; + + explicit EventTemplate(const QJsonObject& json) + : BaseEventT(json) + {} + explicit EventTemplate(const ContentT& c) + : BaseEventT(EventT::basicJson(EventT::TypeId, toJson(c))) + {} + + ContentT content() const { return fromJson(this->contentJson()); } +}; + //! \brief Supply event metatype information in base event types //! //! Use this macro in a public section of your base event class to provide @@ -445,19 +499,19 @@ using Events = EventsArray; /// To retrieve the value the getter uses a JSON key name that corresponds to /// its own (getter's) name but written in snake_case. \p GetterName_ must be /// in camelCase, no quotes (an identifier, not a literal). -#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ - JsonKey_) \ - class QUOTIENT_API Name_ : public Base_ { \ - public: \ - QUO_EVENT(Name_, TypeId_) \ - using value_type = ValueType_; \ - explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \ - explicit Name_(const value_type& v) \ - : Name_(Base_::basicJson(TypeId, { { JsonKey, toJson(v) } })) \ - {} \ - QUO_CONTENT_GETTER_X(ValueType_, GetterName_, JsonKey) \ - static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \ - }; \ +#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ + JsonKey_) \ + constexpr auto Name_##ContentKey = JsonKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public EventTemplate< \ + Name_, Base_, \ + EventContent::SingleKeyValue> { \ + public: \ + QUO_EVENT(Name_, TypeId_) \ + using value_type = ValueType_; \ + using EventTemplate::EventTemplate; \ + QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey) \ + }; \ // End of macro // === is<>(), eventCast<>() and switchOnType<>() === diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 4c639efa..b4ac154c 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -6,40 +6,8 @@ #include "stateevent.h" namespace Quotient { - -/*! Create an event with proper type from a JSON object - * - * Use this factory template to detect the type from the JSON object - * contents (the detected event type should derive from the template - * parameter type) and create an event object of that type. - */ -template -inline event_ptr_tt loadEvent(const QJsonObject& fullJson) -{ - return doLoadEvent(fullJson, fullJson[TypeKeyL].toString()); -} - -//! \brief Create an event from a type string and content JSON -//! -//! Use this template to resolve the C++ type from the Matrix type string in -//! \p matrixType and create an event of that type by passing all parameters -//! to BaseEventT::basicJson(). -template -inline event_ptr_tt loadEvent( - const QString& matrixType, const BasicJsonParamTs&... basicJsonParams) -{ - return doLoadEvent( - BaseEventT::basicJson(matrixType, basicJsonParams...), matrixType); +struct [[deprecated( + "This header is obsolete since libQuotient 0.7; include a header with" + " the respective event type definition instead")]] EventLoaderH; +StateEventPtr eventLoaderH(EventLoaderH&); } - -template -struct JsonConverter> - : JsonObjectUnpacker> { - using JsonObjectUnpacker>::load; - static auto load(const QJsonObject& jo) - { - return loadEvent(jo); - } -}; - -} // namespace Quotient diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 5b5a518f..0ffd8b2c 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -15,9 +15,7 @@ class QUOTIENT_API KeyVerificationRequestEvent : public Event { public: QUO_EVENT(KeyVerificationRequestEvent, "m.key.verification.request") - explicit KeyVerificationRequestEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationRequestEvent(const QString& transactionId, const QString& fromDevice, const QStringList& methods, @@ -50,9 +48,7 @@ class QUOTIENT_API KeyVerificationReadyEvent : public Event { public: QUO_EVENT(KeyVerificationReadyEvent, "m.key.verification.ready") - explicit KeyVerificationReadyEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationReadyEvent(const QString& transactionId, const QString& fromDevice, const QStringList& methods) @@ -77,9 +73,7 @@ class QUOTIENT_API KeyVerificationStartEvent : public Event { public: QUO_EVENT(KeyVerificationStartEvent, "m.key.verification.start") - explicit KeyVerificationStartEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationStartEvent(const QString& transactionId, const QString& fromDevice) : KeyVerificationStartEvent( @@ -150,9 +144,7 @@ class QUOTIENT_API KeyVerificationAcceptEvent : public Event { public: QUO_EVENT(KeyVerificationAcceptEvent, "m.key.verification.accept") - explicit KeyVerificationAcceptEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationAcceptEvent(const QString& transactionId, const QString& commitment) : KeyVerificationAcceptEvent(basicJson( @@ -200,9 +192,7 @@ class QUOTIENT_API KeyVerificationCancelEvent : public Event { public: QUO_EVENT(KeyVerificationCancelEvent, "m.key.verification.cancel") - explicit KeyVerificationCancelEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationCancelEvent(const QString& transactionId, const QString& reason) : KeyVerificationCancelEvent( @@ -230,9 +220,7 @@ class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key") - explicit KeyVerificationKeyEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationKeyEvent(const QString& transactionId, const QString& key) : KeyVerificationKeyEvent( basicJson(TypeId, { { "transaction_id"_ls, transactionId }, @@ -251,9 +239,7 @@ class QUOTIENT_API KeyVerificationMacEvent : public Event { public: QUO_EVENT(KeyVerificationMacEvent, "m.key.verification.mac") - explicit KeyVerificationMacEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; KeyVerificationMacEvent(const QString& transactionId, const QString& keys, const QJsonObject& mac) : KeyVerificationMacEvent( @@ -278,9 +264,7 @@ class QUOTIENT_API KeyVerificationDoneEvent : public Event { public: QUO_EVENT(KeyVerificationDoneEvent, "m.key.verification.done") - explicit KeyVerificationDoneEvent(const QJsonObject& obj) - : Event(TypeId, obj) - {} + using Event::Event; explicit KeyVerificationDoneEvent(const QString& transactionId) : KeyVerificationDoneEvent( basicJson(TypeId, { { "transaction_id"_ls, transactionId } })) diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 7f06d99f..d8f9fa0b 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -28,7 +28,7 @@ using namespace Quotient; // map lookups are not used and vectors are massively faster. Same goes for // de-/serialization of ReceiptsForEvent::receipts. // (XXX: would this be generally preferred across CS API JSON maps?..) -QJsonObject toJson(const EventsWithReceipts& ewrs) +QJsonObject Quotient::toJson(const EventsWithReceipts& ewrs) { QJsonObject json; for (const auto& e : ewrs) { @@ -41,20 +41,16 @@ QJsonObject toJson(const EventsWithReceipts& ewrs) return json; } -ReceiptEvent::ReceiptEvent(const EventsWithReceipts &ewrs) - : Event(typeId(), matrixTypeId(), toJson(ewrs)) -{} - -EventsWithReceipts ReceiptEvent::eventsWithReceipts() const +template<> +EventsWithReceipts Quotient::fromJson(const QJsonObject& json) { EventsWithReceipts result; - const auto& contents = contentJson(); - result.reserve(contents.size()); - for (auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt) { + result.reserve(json.size()); + for (auto eventIt = json.begin(); eventIt != json.end(); ++eventIt) { if (eventIt.key().isEmpty()) { qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; - qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; + qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << json; continue; } const auto reads = diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index a02f4592..b87e00f6 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -19,12 +19,17 @@ struct ReceiptsForEvent { }; using EventsWithReceipts = QVector; -class QUOTIENT_API ReceiptEvent : public Event { +template <> +QUOTIENT_API EventsWithReceipts fromJson(const QJsonObject& json); +QUOTIENT_API QJsonObject toJson(const EventsWithReceipts& ewrs); + +class QUOTIENT_API ReceiptEvent + : public EventTemplate { public: QUO_EVENT(ReceiptEvent, "m.receipt") - explicit ReceiptEvent(const EventsWithReceipts& ewrs); - explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {} + using EventTemplate::EventTemplate; - EventsWithReceipts eventsWithReceipts() const; + [[deprecated("Use content() instead")]] + EventsWithReceipts eventsWithReceipts() const { return content(); } }; } // namespace Quotient diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index c193054a..a2e0b73b 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -10,8 +10,7 @@ class QUOTIENT_API RedactionEvent : public RoomEvent { public: QUO_EVENT(RedactionEvent, "m.room.redaction") - explicit RedactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) - {} + using RoomEvent::RoomEvent; QString redactedEvent() const { diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 2ebe29bf..1986f852 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -8,26 +8,15 @@ namespace Quotient { class QUOTIENT_API RoomAvatarEvent - : public StateEvent { + : public KeylessStateEventBase { // It's a bit of an overkill to use a full-fledged ImageContent // because in reality m.room.avatar usually only has a single URL, // without a thumbnail. But The Spec says there be thumbnails, and - // we follow The Spec. + // we follow The Spec (and ImageContent is very convenient to reuse here). public: QUO_EVENT(RoomAvatarEvent, "m.room.avatar") - explicit RoomAvatarEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) - {} - explicit RoomAvatarEvent(const EventContent::ImageContent& avatar) - : StateEvent(typeId(), matrixTypeId(), QString(), avatar) - {} - // A replica of EventContent::ImageInfo constructor - explicit RoomAvatarEvent(const QUrl& mxcUrl, qint64 fileSize = -1, - QMimeType mimeType = {}, - const QSize& imageSize = {}, - const QString& originalFilename = {}) - : RoomAvatarEvent(EventContent::ImageContent { - mxcUrl, fileSize, mimeType, imageSize, originalFilename }) - {} + using KeylessStateEventBase::KeylessStateEventBase; QUrl url() const { return content().url(); } }; diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index e1c7888e..c73bc92a 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -32,25 +32,11 @@ inline auto toJson(const EventContent::AliasesEventContent& c) } class QUOTIENT_API RoomCanonicalAliasEvent - : public StateEvent { + : public KeylessStateEventBase { public: QUO_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias") - - explicit RoomCanonicalAliasEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - { } - - explicit RoomCanonicalAliasEvent(const QString& canonicalAlias, - const QStringList& altAliases = {}) - : StateEvent(typeId(), matrixTypeId(), {}, - canonicalAlias, altAliases) - { } - - explicit RoomCanonicalAliasEvent(QString&& canonicalAlias, - QStringList&& altAliases = {}) - : StateEvent(typeId(), matrixTypeId(), {}, - std::move(canonicalAlias), std::move(altAliases)) - { } + using KeylessStateEventBase::KeylessStateEventBase; QString alias() const { return content().canonicalAlias; } QStringList altAliases() const { return content().altAliases; } diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index f22752b4..2709258f 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -11,9 +11,7 @@ class QUOTIENT_API RoomCreateEvent : public StateEventBase { public: QUO_EVENT(RoomCreateEvent, "m.room.create") - explicit RoomCreateEvent(const QJsonObject& obj) - : StateEventBase(typeId(), obj) - {} + using StateEventBase::StateEventBase; struct Predecessor { QString roomId; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index e695e0ec..bd06f5c5 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -8,12 +8,7 @@ using namespace Quotient; -RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson) - : Event(type, matrixType, contentJson) -{} - -RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) +RoomEvent::RoomEvent(const QJsonObject& json) : Event(json) { if (const auto redaction = unsignedPart(RedactedCauseKeyL); !redaction.isEmpty()) @@ -110,14 +105,8 @@ QJsonObject CallEventBase::basicJson(const QString& matrixType, return RoomEvent::basicJson(matrixType, contentJson); } -CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, - const QString& callId, int version, - const QJsonObject& contentJson) - : RoomEvent(type, basicJson(matrixType, callId, version, contentJson)) -{} - -CallEventBase::CallEventBase(Type type, const QJsonObject& json) - : RoomEvent(type, json) +CallEventBase::CallEventBase(const QJsonObject& json) + : RoomEvent(json) { if (callId().isEmpty()) qCWarning(EVENTS) << id() << "is a call event with an empty call id"; diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 532e72e2..830f1d30 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -17,10 +17,8 @@ public: QUO_BASE_EVENT(RoomEvent, {}, Event::BaseMetaType) // RedactionEvent is an incomplete type here so we cannot inline - // constructors and destructors and we cannot use 'using'. - RoomEvent(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); - RoomEvent(Type type, const QJsonObject& json); + // constructors using it and also destructors (with 'using', in particular). + explicit RoomEvent(const QJsonObject& json); ~RoomEvent() override; QString id() const; @@ -85,10 +83,7 @@ class QUOTIENT_API CallEventBase : public RoomEvent { public: QUO_BASE_EVENT(CallEventBase, "m.call.*"_ls, RoomEvent::BaseMetaType) - CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, - int version, const QJsonObject& contentJson = {}); - CallEventBase(Type type, const QJsonObject& json); - ~CallEventBase() override = default; + explicit CallEventBase(const QJsonObject& json); QUO_CONTENT_GETTER(QString, callId) QUO_CONTENT_GETTER(int, version) @@ -98,6 +93,17 @@ protected: const QString& callId, int version, QJsonObject contentJson = {}); }; + +template +class EventTemplate : public CallEventBase { +public: + using CallEventBase::CallEventBase; + explicit EventTemplate(const QString& callId, + const QJsonObject& contentJson = {}) + : EventTemplate(basicJson(EventT::TypeId, callId, 0, contentJson)) + {} +}; + } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) Q_DECLARE_METATYPE(const Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp deleted file mode 100644 index 3a8601d1..00000000 --- a/lib/events/roomkeyevent.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Alexey Andreyev -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "roomkeyevent.h" - -using namespace Quotient; - -RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(TypeId, obj) -{ - if (roomId().isEmpty()) - qCWarning(E2EE) << "Room key event has empty room id"; -} - -RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, - const QString& sessionId, const QString& sessionKey) - : Event(TypeId, basicJson(TypeId, { - { "algorithm", algorithm }, - { "room_id", roomId }, - { "session_id", sessionId }, - { "session_key", sessionKey }, - })) -{} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 6883a2a5..dad5df8b 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -11,9 +11,16 @@ class QUOTIENT_API RoomKeyEvent : public Event public: QUO_EVENT(RoomKeyEvent, "m.room_key") - explicit RoomKeyEvent(const QJsonObject& obj); + using Event::Event; explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, - const QString& sessionId, const QString& sessionKey); + const QString& sessionId, const QString& sessionKey) + : Event(basicJson(TypeId, { + { "algorithm", algorithm }, + { "room_id", roomId }, + { "session_id", sessionId }, + { "session_key", sessionKey }, + })) + {} QUO_CONTENT_GETTER(QString, algorithm) QUO_CONTENT_GETTER(QString, roomId) diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index c690586e..9f063136 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -28,7 +28,8 @@ public: using MembershipType [[deprecated("Use Membership instead")]] = Membership; -class QUOTIENT_API RoomMemberEvent : public StateEvent { +class QUOTIENT_API RoomMemberEvent + : public KeyedStateEventBase { Q_GADGET public: QUO_EVENT(RoomMemberEvent, "m.room.member") @@ -36,24 +37,7 @@ public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) - {} - RoomMemberEvent(const QString& userId, MemberEventContent&& content) - : StateEvent(typeId(), matrixTypeId(), userId, std::move(content)) - {} - - //! \brief A special constructor to create unknown RoomMemberEvents - //! - //! This is needed in order to use RoomMemberEvent as a "base event class" - //! in cases like GetMembersByRoomJob when RoomMemberEvents (rather than - //! RoomEvents or StateEvents) are resolved from JSON. For such cases - //! loadEvent\<> requires an underlying class to have a specialisation of - //! EventFactory\<> and be constructible with unknownTypeId() instead of - //! its genuine id. Don't use directly. - //! \sa EventFactory, loadEvent, GetMembersByRoomJob - RoomMemberEvent(Type type, const QJsonObject& fullJson) - : StateEvent(type, fullJson) - {} + using KeyedStateEventBase::KeyedStateEventBase; Membership membership() const { return content().membership; } QString userId() const { return stateKey(); } diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 2a6ae93c..db5afaf1 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -128,8 +128,9 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) - : RoomEvent(typeId(), matrixTypeId(), - assembleContentJson(plainBody, jsonMsgType, content)) + : RoomEvent(RoomEvent::basicJson(TypeId, + assembleContentJson(plainBody, jsonMsgType, + content))) , _content(content) {} @@ -175,7 +176,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, #endif RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj), _content(nullptr) + : RoomEvent(obj), _content(nullptr) { if (isRedacted()) return; diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index 7ac12db0..6150980a 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -31,16 +31,11 @@ struct QUOTIENT_API PowerLevelsEventContent { }; class QUOTIENT_API RoomPowerLevelsEvent - : public StateEvent { + : public KeylessStateEventBase { public: QUO_EVENT(RoomPowerLevelsEvent, "m.room.power_levels") - explicit RoomPowerLevelsEvent(PowerLevelsEventContent&& content) - : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) - {} - explicit RoomPowerLevelsEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - {} + using KeylessStateEventBase::KeylessStateEventBase; int invite() const { return content().invite; } int kick() const { return content().kick; } diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 97586587..95743e32 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -10,9 +10,7 @@ class QUOTIENT_API RoomTombstoneEvent : public StateEventBase { public: QUO_EVENT(RoomTombstoneEvent, "m.room.tombstone") - explicit RoomTombstoneEvent(const QJsonObject& obj) - : StateEventBase(typeId(), obj) - {} + using StateEventBase::StateEventBase; QString serverMessage() const; QString successorRoomId() const; diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index c79d03b0..d84dc1b1 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -7,25 +7,18 @@ #include "single_key_value.h" namespace Quotient { -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - constexpr auto _Name##Key = #_ContentKey##_ls; \ - class QUOTIENT_API _Name \ - : public StateEvent< \ - EventContent::SingleKeyValue<_ValueType, &_Name##Key>> { \ - public: \ - using value_type = _ValueType; \ - QUO_EVENT(_Name, _TypeId) \ - template \ - explicit _Name(T&& value) \ - : StateEvent(TypeId, matrixTypeId(), QString(), \ - std::forward(value)) \ - {} \ - explicit _Name(QJsonObject obj) \ - : StateEvent(TypeId, std::move(obj)) \ - {} \ - auto _ContentKey() const { return content().value; } \ - }; \ - // End of macro +#define DEFINE_SIMPLE_STATE_EVENT(Name_, TypeId_, ValueType_, ContentKey_) \ + constexpr auto Name_##Key = #ContentKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public KeylessStateEventBase< \ + Name_, EventContent::SingleKeyValue> { \ + public: \ + using value_type = ValueType_; \ + QUO_EVENT(Name_, TypeId_) \ + using KeylessStateEventBase::KeylessStateEventBase; \ + auto ContentKey_() const { return content().value; } \ + }; \ +// End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) @@ -34,13 +27,14 @@ DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", constexpr auto RoomAliasesEventKey = "aliases"_ls; class QUOTIENT_API RoomAliasesEvent - : public StateEvent< - EventContent::SingleKeyValue> { + : public KeyedStateEventBase< + RoomAliasesEvent, + EventContent::SingleKeyValue> +{ public: QUO_EVENT(RoomAliasesEvent, "m.room.aliases") - explicit RoomAliasesEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - {} + using KeyedStateEventBase::KeyedStateEventBase; + Q_DECL_DEPRECATED_X( "m.room.aliases events are deprecated by the Matrix spec; use" " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 1df24df0..e117f8a0 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -5,18 +5,16 @@ using namespace Quotient; -StateEventBase::StateEventBase(Type type, const QJsonObject& json) - : RoomEvent(json.contains(StateKeyKeyL) ? type : UnknownEventTypeId, json) +StateEventBase::StateEventBase(const QJsonObject& json) + : RoomEvent(json) { - if (Event::type() == UnknownEventTypeId && !json.contains(StateKeyKeyL)) - qWarning(EVENTS) << "Attempt to create a state event with no stateKey -" - "forcing the event type to unknown to avoid damage"; + Q_ASSERT_X(json.contains(StateKeyKeyL), __FUNCTION__, + "Attempt to create a state event without state key"); } -StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, - const QString& stateKey, +StateEventBase::StateEventBase(Event::Type type, const QString& stateKey, const QJsonObject& contentJson) - : RoomEvent(type, basicJson(type, stateKey, contentJson)) + : RoomEvent(basicJson(type, stateKey, contentJson)) {} bool StateEventBase::repeatsState() const diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 74876803..911972f2 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -16,11 +16,17 @@ public: return fullJson.contains(StateKeyKeyL); } - StateEventBase(Type type, const QJsonObject& json); - StateEventBase(Type type, event_mtype_t matrixType, - const QString& stateKey = {}, - const QJsonObject& contentJson = {}); - ~StateEventBase() override = default; + //! \brief Static setting of whether a given even type uses state keys + //! + //! Most event types don't use a state key; overriding this to `true` + //! for a given type changes the calls across Quotient to include state key + //! in their signatures; otherwise, state key is still accessible but + //! constructors and calls in, e.g., RoomStateView don't include it. + static constexpr auto needsStateKey = false; + + explicit StateEventBase(const QJsonObject& json); + explicit StateEventBase(Type type, const QString& stateKey = {}, + const QJsonObject& contentJson = {}); //! Make a minimal correct Matrix state event JSON static QJsonObject basicJson(const QString& matrixTypeId, @@ -56,64 +62,85 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, */ using StateEventKey = std::pair; -template -struct Prev { - template - explicit Prev(const QJsonObject& unsignedJson, - ContentParamTs&&... contentParams) - : senderId(unsignedJson.value("prev_sender"_ls).toString()) - , content(fromJson(unsignedJson.value(PrevContentKeyL)), - std::forward(contentParams)...) - {} - - QString senderId; - ContentT content; -}; - -template -class StateEvent : public StateEventBase { +template +class EventTemplate + : public StateEventBase { public: using content_type = ContentT; + struct Prev { + explicit Prev() = default; + explicit Prev(const QJsonObject& unsignedJson) + : senderId(fromJson(unsignedJson["prev_sender"_ls])) + , content( + fromJson>(unsignedJson[PrevContentKeyL])) + {} + + QString senderId; + Omittable content; + }; + + explicit EventTemplate(const QJsonObject& fullJson) + : StateEventBase(fullJson) + , _content(fromJson(Event::contentJson())) + , _prev(unsignedJson()) + {} template - explicit StateEvent(Type type, const QJsonObject& fullJson, - ContentParamTs&&... contentParams) - : StateEventBase(type, fullJson) - , _content(fromJson(contentJson()), - std::forward(contentParams)...) - { - const auto& unsignedData = unsignedJson(); - if (unsignedData.contains(PrevContentKeyL)) - _prev = std::make_unique>( - unsignedData, std::forward(contentParams)...); - } - template - explicit StateEvent(Type type, event_mtype_t matrixType, - const QString& stateKey, - ContentParamTs&&... contentParams) - : StateEventBase(type, matrixType, stateKey) - , _content{std::forward(contentParams)...} + explicit EventTemplate(const QString& stateKey, + ContentParamTs&&... contentParams) + : StateEventBase(EventT::TypeId, stateKey) + , _content { std::forward(contentParams)... } { editJson().insert(ContentKey, toJson(_content)); } const ContentT& content() const { return _content; } + template void editContent(VisitorT&& visitor) { visitor(_content); editJson()[ContentKeyL] = toJson(_content); } - const ContentT* prevContent() const - { - return _prev ? &_prev->content : nullptr; - } - QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } + const Omittable& prevContent() const { return _prev.content; } + QString prevSenderId() const { return _prev.senderId; } private: ContentT _content; - std::unique_ptr> _prev; + Prev _prev; }; + +template +class KeyedStateEventBase + : public EventTemplate { +public: + static constexpr auto needsStateKey = true; + + using EventTemplate::EventTemplate; +}; + +template +concept Keyed_State_Event = EvT::needsStateKey; + +template +class KeylessStateEventBase + : public EventTemplate { +private: + using base_type = EventTemplate; + +public: + explicit KeylessStateEventBase(const QJsonObject& fullJson) + : base_type(fullJson) + {} + template + explicit KeylessStateEventBase(ContentParamTs&&... contentParams) + : base_type(QString(), std::forward(contentParams)...) + {} +}; + +template +concept Keyless_State_Event = !EvT::needsStateKey; + } // namespace Quotient Q_DECLARE_METATYPE(Quotient::StateEventBase*) Q_DECLARE_METATYPE(const Quotient::StateEventBase*) 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()) diff --git a/lib/roomstateview.h b/lib/roomstateview.h index 29cce00e..13b375f2 100644 --- a/lib/roomstateview.h +++ b/lib/roomstateview.h @@ -11,6 +11,16 @@ namespace Quotient { class Room; +// NB: Both concepts below expect EvT::needsStateKey to exist so you can't +// express one via negation of the other (there's still an invalid case of +// a non-state event where needsStateKey is not even defined). + +template >> +concept Keyed_State_Fn = EvT::needsStateKey; + +template >> +concept Keyless_State_Fn = !EvT::needsStateKey; + class QUOTIENT_API RoomStateView : private QHash { Q_GADGET @@ -36,31 +46,48 @@ public: //! \brief Get a state event with the given event type and state key //! //! This is a typesafe overload that accepts a C++ event type instead of - //! its Matrix name. - //! \warning In libQuotient 0.7 the return type changed to an Omittable with - //! a reference wrapper inside - you have to check that it - //! has_value() before using. Alternatively you can now use - //! queryCurrentState() to access state safely. - template + //! its Matrix name. It is only defined for events with state key (i.e., + //! derived from KeyedStateEvent). + template const EvT* get(const QString& stateKey = {}) const { - static_assert(std::is_base_of_v); - if (const auto* evt = get(EvT::matrixTypeId(), stateKey)) { - Q_ASSERT(evt->matrixType() == EvT::matrixTypeId() + if (const auto* evt = get(EvT::TypeId, stateKey)) { + Q_ASSERT(evt->matrixType() == EvT::TypeId && evt->stateKey() == stateKey); return eventCast(evt); } return nullptr; } + //! \brief Get a state event with the given event type + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. This overload only defined for events that do not use + //! state key (i.e., derived from KeylessStateEvent). + template + const EvT* get() const + { + if (const auto* evt = get(EvT::TypeId)) { + Q_ASSERT(evt->matrixType() == EvT::TypeId); + return eventCast(evt); + } + return nullptr; + } + using QHash::contains; bool contains(const QString& evtType, const QString& stateKey = {}) const; - template + template bool contains(const QString& stateKey = {}) const { - return contains(EvT::matrixTypeId(), stateKey); + return contains(EvT::TypeId, stateKey); + } + + template + bool contains() const + { + return contains(EvT::TypeId); } //! \brief Get the content of the current state event with the given @@ -78,48 +105,85 @@ public: const QVector eventsOfType(const QString& evtType) const; + //! \brief Run a function on a state event with the given type and key + //! + //! Use this overload when there's no predefined event type or the event + //! type is unknown at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails template auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const { return lift(std::forward(fn), get(evtType, stateKey)); } - template + //! \brief Run a function on a state event with the given type and key + //! + //! This is an overload for keyed state events (those that have + //! `needsStateKey == true`) with type defined at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template auto query(const QString& stateKey, FnT&& fn) const { using EventT = std::decay_t>; - static_assert(std::is_base_of_v); return lift(std::forward(fn), get(stateKey)); } + //! \brief Run a function on a keyless state event with the given type + //! + //! This is an overload for keyless state events (those having + //! `needsStateKey == false`) with type defined at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template + auto query(FnT&& fn) const + { + using EventT = std::decay_t>; + return lift(std::forward(fn), get()); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails template auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn, FallbackT&& fallback) const { - return lift(std::forward(fn), get(evtType, stateKey)) + return query(evtType, stateKey, std::forward(fn)) .value_or(std::forward(fallback)); } - template - auto query(FnT&& fn) const - { - return query({}, std::forward(fn)); - } - + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails template auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const { - using EventT = std::decay_t>; - static_assert(std::is_base_of_v); - return lift(std::forward(fn), get(stateKey)) + return query(stateKey, std::forward(fn)) .value_or(std::forward(fallback)); } + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails template auto queryOr(FnT&& fn, FallbackT&& fallback) const { - return queryOr({}, std::forward(fn), - std::forward(fallback)); + return query(std::forward(fn)) + .value_or(std::forward(fallback)); } private: diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 93416bc4..eb6c932b 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -3,8 +3,6 @@ #include "syncdata.h" -#include "events/eventloader.h" - #include #include -- cgit v1.2.3 From 896f46d4cf08ab05611b72edeb6c4d70e597342f Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 12 Aug 2022 17:07:20 +0200 Subject: GTAD: Stop using eventloader.h in generated files --- gtad/gtad.yaml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 0bec3b7a..4b05d2d4 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -84,11 +84,16 @@ analyzer: - $ref: - +set: moveOnly: - imports: '"events/eventloader.h"' +on: - - /state_event.yaml$/: StateEventPtr - - /(room|client)_event.yaml$/: RoomEventPtr - - /event(_without_room_id)?.yaml$/: EventPtr + - /state_event.yaml$/: + type: StateEventPtr + imports: '"events/stateevent.h"' + - /(room|client)_event.yaml$/: + type: RoomEventPtr + imports: '"events/roomevent.h"' + - /event(_without_room_id)?.yaml$/: + type: EventPtr + imports: '"events/event.h"' - +set: # This renderer applies to everything actually $ref'ed # (not substituted) @@ -132,12 +137,11 @@ analyzer: - +set: { moveOnly: } +on: - /^Notification|Result|ChildRoomsChunk$/: "std::vector<{{1}}>" - - StrippedChildStateEvent: + - /^StrippedChildStateEvent$|state_event.yaml$/: type: StateEvents - imports: '"events/eventloader.h"' - - /state_event.yaml$/: StateEvents # 'imports' already set under $ref - - /(room|client)_event.yaml$/: RoomEvents # ditto - - /event(_without_room_id)?.yaml$/: Events # ditto + imports: '"events/stateevent.h"' # For StrippedChildStateEvent + - /(room|client)_event.yaml$/: RoomEvents + - /event(_without_room_id)?.yaml$/: Events - //: "QVector<{{1}}>" - map: # `additionalProperties` in OpenAPI - RoomState: -- cgit v1.2.3 From d5eef705b2b5d9dec17d72ab27cbdd48a1391485 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 12 Aug 2022 17:07:55 +0200 Subject: Regenerate CS API upon GTAD config change --- lib/csapi/event_context.h | 3 ++- lib/csapi/message_pagination.h | 2 +- lib/csapi/notifications.h | 2 +- lib/csapi/peeking_events.h | 2 +- lib/csapi/relations.h | 2 +- lib/csapi/rooms.h | 3 ++- lib/csapi/search.h | 3 ++- lib/csapi/space_hierarchy.h | 2 +- 8 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index 662b976b..1614c7ed 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -4,7 +4,8 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 9831ae2d..b4f3a38a 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 48167877..ff8aa47f 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/event.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index ff688c49..a67d2e4a 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h index 985a43b5..794ae445 100644 --- a/lib/csapi/relations.h +++ b/lib/csapi/relations.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 247fb13f..7823a1b0 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -4,7 +4,8 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 8683413d..30095f32 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -6,7 +6,8 @@ #include "csapi/definitions/room_event_filter.h" -#include "events/eventloader.h" +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h index 7a421be8..e5da6df2 100644 --- a/lib/csapi/space_hierarchy.h +++ b/lib/csapi/space_hierarchy.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/stateevent.h" #include "jobs/basejob.h" namespace Quotient { -- cgit v1.2.3 From 3fa9afc796788e0c7f44db15c21a5ba1294d3b2d Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 25 Jul 2022 17:05:59 +0200 Subject: Remove #include "logging.h" from event.h We don't expose logging internals to the outside world. --- lib/events/encryptedevent.cpp | 1 + lib/events/encryptionevent.cpp | 1 + lib/events/event.h | 1 - lib/events/roommemberevent.cpp | 3 +-- lib/events/stateevent.cpp | 1 + lib/syncdata.cpp | 2 ++ 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index c539d5b2..e9b4a585 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" +#include "logging.h" using namespace Quotient; diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 8872447b..b1b04984 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptionevent.h" +#include "logging.h" #include "e2ee/e2ee.h" diff --git a/lib/events/event.h b/lib/events/event.h index 8a8d64b0..4b715813 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -4,7 +4,6 @@ #pragma once #include "converters.h" -#include "logging.h" #include "function_traits.h" #include "single_key_value.h" diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 953ff8ae..4e7eae1b 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -3,8 +3,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "roommemberevent.h" - -#include +#include "logging.h" namespace Quotient { template <> diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index e117f8a0..204044bb 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "stateevent.h" +#include "logging.h" using namespace Quotient; diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index eb6c932b..ec7203af 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -3,6 +3,8 @@ #include "syncdata.h" +#include "logging.h" + #include #include -- 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/events/encryptedevent.cpp | 7 ++++--- lib/events/encryptedevent.h | 7 ++++--- lib/events/event.h | 2 +- lib/events/roommessageevent.cpp | 5 ++--- lib/expected.h | 7 ++++--- lib/room.cpp | 16 ++++++++-------- lib/room.h | 7 +++---- quotest/quotest.cpp | 2 +- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index e9b4a585..94b44901 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -6,14 +6,15 @@ using namespace Quotient; -EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext, +EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertexts, const QString& senderKey) : RoomEvent({ { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, - { CiphertextKeyL, ciphertext }, + { CiphertextKeyL, ciphertexts }, { SenderKeyKeyL, senderKey } }) {} -EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, +EncryptedEvent::EncryptedEvent(const QByteArray& ciphertext, + const QString& senderKey, const QString& deviceId, const QString& sessionId) : RoomEvent({ { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 22e51cb8..02d4c7aa 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -32,11 +32,12 @@ public: /* In case with Olm, the encrypted content of the event is * a map from the recipient Curve25519 identity key to ciphertext * information */ - explicit EncryptedEvent(const QJsonObject& ciphertext, + explicit EncryptedEvent(const QJsonObject& ciphertexts, const QString& senderKey); /* In case with Megolm, device_id and session_id are required */ - explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey, - const QString& deviceId, const QString& sessionId); + explicit EncryptedEvent(const QByteArray& ciphertext, + const QString& senderKey, const QString& deviceId, + const QString& sessionId); explicit EncryptedEvent(const QJsonObject& obj); QString algorithm() const; diff --git a/lib/events/event.h b/lib/events/event.h index 4b715813..ea5a2554 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -269,7 +269,7 @@ public: explicit Event(const QJsonObject& json); Q_DISABLE_COPY(Event) - Event(Event&&) = default; + Event(Event&&) noexcept = default; Event& operator=(Event&&) = delete; virtual ~Event(); diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index db5afaf1..df4840b3 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -128,9 +128,8 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) - : RoomEvent(RoomEvent::basicJson(TypeId, - assembleContentJson(plainBody, jsonMsgType, - content))) + : RoomEvent( + basicJson(TypeId, assembleContentJson(plainBody, jsonMsgType, content))) , _content(content) {} diff --git a/lib/expected.h b/lib/expected.h index 7b9e7f1d..81e186ea 100644 --- a/lib/expected.h +++ b/lib/expected.h @@ -21,11 +21,12 @@ public: using error_type = E; Expected() = default; - explicit Expected(const Expected&) = default; - explicit Expected(Expected&&) noexcept = default; + Expected(const Expected&) = default; + Expected(Expected&&) noexcept = default; + ~Expected() = default; template > - Expected(X&& x) + QUO_IMPLICIT Expected(X&& x) // NOLINT(google-explicit-constructor) : data(std::forward(x)) {} 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"), diff --git a/lib/room.h b/lib/room.h index 44504691..b454bfbc 100644 --- a/lib/room.h +++ b/lib/room.h @@ -770,11 +770,10 @@ public: "make sure to check its result for nullptrs")]] // const EvT* getCurrentState(const QString& stateKey = {}) const { - QT_IGNORE_DEPRECATIONS( - const auto* evt = eventCast( - getCurrentState(EvT::matrixTypeId(), stateKey));) + QT_IGNORE_DEPRECATIONS(const auto* evt = eventCast( + getCurrentState(EvT::TypeId, stateKey));) Q_ASSERT(evt); - Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() + Q_ASSERT(evt->matrixType() == EvT::TypeId && evt->stateKey() == stateKey); return evt; } diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 3860ae1e..3ac6404a 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -163,7 +163,7 @@ bool TestSuite::validatePendingEvent(const QString& txnId) return it != targetRoom->pendingEvents().end() && it->deliveryStatus() == EventStatus::Submitted && (*it)->transactionId() == txnId && is(**it) - && (*it)->matrixType() == EventT::matrixTypeId(); + && (*it)->matrixType() == EventT::TypeId; } void TestSuite::finishTest(const TestToken& token, bool condition, -- 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 ++--- lib/roomstateview.h | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) 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 diff --git a/lib/roomstateview.h b/lib/roomstateview.h index 13b375f2..119c24cf 100644 --- a/lib/roomstateview.h +++ b/lib/roomstateview.h @@ -90,6 +90,26 @@ public: return contains(EvT::TypeId); } + template + auto content(const QString& stateKey, + typename EvT::content_type defaultValue = {}) const + { + // StateEvent<>::content is special in that it returns a const-ref, + // and lift() inside queryOr() can't wrap that in a temporary Omittable. + if (const auto evt = get(stateKey)) + return evt->content(); + return std::move(defaultValue); + } + + template + auto content(typename EvT::content_type defaultValue = {}) const + { + // Same as above + if (const auto evt = get()) + return evt->content(); + return defaultValue; + } + //! \brief Get the content of the current state event with the given //! event type and state key //! \return An empty object if there's no event in the current state with -- 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. --- CMakeLists.txt | 5 +- autotests/callcandidateseventtest.cpp | 2 +- lib/eventitem.h | 5 +- lib/events/callanswerevent.cpp | 34 ------------ lib/events/callanswerevent.h | 24 --------- lib/events/callcandidatesevent.h | 27 ---------- lib/events/callevents.cpp | 82 +++++++++++++++++++++++++++++ lib/events/callevents.h | 99 +++++++++++++++++++++++++++++++++++ lib/events/callhangupevent.h | 17 ------ lib/events/callinviteevent.cpp | 37 ------------- lib/events/callinviteevent.h | 26 --------- lib/events/event.cpp | 3 +- lib/events/event.h | 8 +-- lib/events/roomevent.cpp | 16 ------ lib/events/roomevent.h | 25 --------- lib/room.cpp | 7 +-- 16 files changed, 194 insertions(+), 223 deletions(-) delete mode 100644 lib/events/callanswerevent.cpp delete mode 100644 lib/events/callanswerevent.h delete mode 100644 lib/events/callcandidatesevent.h create mode 100644 lib/events/callevents.cpp create mode 100644 lib/events/callevents.h delete mode 100644 lib/events/callhangupevent.h delete mode 100644 lib/events/callinviteevent.cpp delete mode 100644 lib/events/callinviteevent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d4cf52d0..1c4d9545 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,10 +158,7 @@ list(APPEND lib_SRCS lib/events/accountdataevents.h lib/events/receiptevent.h lib/events/receiptevent.cpp lib/events/reactionevent.h - lib/events/callinviteevent.h lib/events/callinviteevent.cpp - lib/events/callcandidatesevent.h - lib/events/callanswerevent.h lib/events/callanswerevent.cpp - lib/events/callhangupevent.h + lib/events/callevents.h lib/events/callevents.cpp lib/events/directchatevent.h lib/events/directchatevent.cpp lib/events/encryptionevent.h lib/events/encryptionevent.cpp lib/events/encryptedevent.h lib/events/encryptedevent.cpp diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index 0d5a543b..b37dd109 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "events/callcandidatesevent.h" +#include "events/callevents.h" #include diff --git a/lib/eventitem.h b/lib/eventitem.h index 90d9f458..2e55a724 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -5,6 +5,7 @@ #include "quotient_common.h" +#include "events/callevents.h" #include "events/filesourceinfo.h" #include "events/stateevent.h" @@ -101,9 +102,9 @@ inline const StateEventBase* EventItemBase::viewAs() const } template <> -inline const CallEventBase* EventItemBase::viewAs() const +inline const CallEvent* EventItemBase::viewAs() const { - return evt->isCallEvent() ? weakPtrCast(evt) : nullptr; + return evt->is() ? weakPtrCast(evt) : nullptr; } class QUOTIENT_API PendingEventItem : public EventItemBase { diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp deleted file mode 100644 index 89dcd7fd..00000000 --- a/lib/events/callanswerevent.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "callanswerevent.h" - -/* -m.call.answer -{ - "age": 242352, - "content": { - "answer": { - "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", - "type": "answer" - }, - "call_id": "12345", - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.answer" -} -*/ - -using namespace Quotient; - -CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) - : EventTemplate(callId, { { QStringLiteral("answer"), - QJsonObject { { QStringLiteral("type"), - QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } }) -{} diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h deleted file mode 100644 index c5ad14df..00000000 --- a/lib/events/callanswerevent.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class QUOTIENT_API CallAnswerEvent - : public EventTemplate { -public: - QUO_EVENT(CallAnswerEvent, "m.call.answer") - - using EventTemplate::EventTemplate; - - explicit CallAnswerEvent(const QString& callId, const QString& sdp); - - QString sdp() const - { - return contentPart("answer"_ls).value("sdp"_ls).toString(); - } -}; -} // namespace Quotient diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h deleted file mode 100644 index f5d2f815..00000000 --- a/lib/events/callcandidatesevent.h +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-FileCopyrightText: 2018 Kitsune Ral -// SPDX-FileCopyrightText: 2020 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class CallCandidatesEvent : public EventTemplate { -public: - QUO_EVENT(CallCandidatesEvent, "m.call.candidates") - - using EventTemplate::EventTemplate; - - explicit CallCandidatesEvent(const QString& callId, - const QJsonArray& candidates) - : EventTemplate(callId, { { QStringLiteral("candidates"), candidates } }) - {} - - QUO_CONTENT_GETTER(QJsonArray, candidates) - QUO_CONTENT_GETTER(QString, callId) - QUO_CONTENT_GETTER(int, version) -}; -} // namespace Quotient diff --git a/lib/events/callevents.cpp b/lib/events/callevents.cpp new file mode 100644 index 00000000..3873614d --- /dev/null +++ b/lib/events/callevents.cpp @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "callevents.h" + +#include "logging.h" + +using namespace Quotient; + +QJsonObject CallEvent::basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson) +{ + contentJson.insert(QStringLiteral("call_id"), callId); + contentJson.insert(QStringLiteral("version"), version); + return RoomEvent::basicJson(matrixType, contentJson); +} + +CallEvent::CallEvent(const QJsonObject& json) + : RoomEvent(json) +{ + if (callId().isEmpty()) + qCWarning(EVENTS) << id() << "is a call event with an empty call id"; +} + +/* +m.call.invite +{ + "age": 242352, + "content": { + "call_id": "12345", + "lifetime": 60000, + "offer": { + "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", + "type": "offer" + }, + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.invite" +} +*/ + +CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, + const QString& sdp) + : EventTemplate( + callId, + { { QStringLiteral("lifetime"), lifetime }, + { QStringLiteral("offer"), + QJsonObject{ { QStringLiteral("type"), QStringLiteral("offer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} + +/* +m.call.answer +{ + "age": 242352, + "content": { + "answer": { + "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", + "type": "answer" + }, + "call_id": "12345", + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.answer" +} +*/ + +CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) + : EventTemplate(callId, { { QStringLiteral("answer"), + QJsonObject { { QStringLiteral("type"), + QStringLiteral("answer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} diff --git a/lib/events/callevents.h b/lib/events/callevents.h new file mode 100644 index 00000000..6d9feae4 --- /dev/null +++ b/lib/events/callevents.h @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" + +namespace Quotient { + +class QUOTIENT_API CallEvent : public RoomEvent { +public: + QUO_BASE_EVENT(CallEvent, "m.call.*"_ls, RoomEvent::BaseMetaType) + static bool matches(const QJsonObject&, const QString& mType) + { + return mType.startsWith("m.call."); + } + + explicit CallEvent(const QJsonObject& json); + + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) + +protected: + static QJsonObject basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson = {}); +}; +using CallEventBase + [[deprecated("CallEventBase is CallEvent now")]] = CallEvent; + +template +class EventTemplate : public CallEvent { +public: + using CallEvent::CallEvent; + explicit EventTemplate(const QString& callId, + const QJsonObject& contentJson = {}) + : EventTemplate(basicJson(EventT::TypeId, callId, 0, contentJson)) + {} +}; + +template +class EventTemplate + : public EventTemplate { +public: + using EventTemplate::EventTemplate; + template + explicit EventTemplate(const QString& callId, + ContentParamTs&&... contentParams) + : EventTemplate( + callId, + toJson(ContentT{ std::forward(contentParams)... })) + {} +}; + +class QUOTIENT_API CallInviteEvent + : public EventTemplate { +public: + QUO_EVENT(CallInviteEvent, "m.call.invite") + + using EventTemplate::EventTemplate; + + explicit CallInviteEvent(const QString& callId, int lifetime, + const QString& sdp); + + QUO_CONTENT_GETTER(int, lifetime) + QString sdp() const + { + return contentPart("offer"_ls).value("sdp"_ls).toString(); + } +}; + +DEFINE_SIMPLE_EVENT(CallCandidatesEvent, CallEvent, "m.call.candidates", + QJsonArray, candidates, "candidates") + +class QUOTIENT_API CallAnswerEvent + : public EventTemplate { +public: + QUO_EVENT(CallAnswerEvent, "m.call.answer") + + using EventTemplate::EventTemplate; + + explicit CallAnswerEvent(const QString& callId, const QString& sdp); + + QString sdp() const + { + return contentPart("answer"_ls).value("sdp"_ls).toString(); + } +}; + +class QUOTIENT_API CallHangupEvent + : public EventTemplate { +public: + QUO_EVENT(CallHangupEvent, "m.call.hangup") + using EventTemplate::EventTemplate; +}; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::CallEvent*) +Q_DECLARE_METATYPE(const Quotient::CallEvent*) diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h deleted file mode 100644 index f0b131b9..00000000 --- a/lib/events/callhangupevent.h +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class QUOTIENT_API CallHangupEvent - : public EventTemplate { -public: - QUO_EVENT(CallHangupEvent, "m.call.hangup") - using EventTemplate::EventTemplate; -}; -//REGISTER_EVENT_TYPE(CallHangupEvent) -} // namespace Quotient diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp deleted file mode 100644 index 0232275b..00000000 --- a/lib/events/callinviteevent.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "callinviteevent.h" - -/* -m.call.invite -{ - "age": 242352, - "content": { - "call_id": "12345", - "lifetime": 60000, - "offer": { - "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", - "type": "offer" - }, - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.invite" -} -*/ - -using namespace Quotient; - -CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, - const QString& sdp) - : EventTemplate(callId, - { { QStringLiteral("lifetime"), lifetime }, - { QStringLiteral("offer"), - QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") }, - { QStringLiteral("sdp"), sdp } } } }) -{} diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h deleted file mode 100644 index fc22f7e1..00000000 --- a/lib/events/callinviteevent.h +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard -// SPDX-FileCopyrightText: 2018 Josip Delic -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class QUOTIENT_API CallInviteEvent - : public EventTemplate { -public: - QUO_EVENT(CallInviteEvent, "m.call.invite") - - using EventTemplate::EventTemplate; - - explicit CallInviteEvent(const QString& callId, int lifetime, - const QString& sdp); - - QUO_CONTENT_GETTER(int, lifetime) - QString sdp() const - { - return contentPart("offer"_ls).value("sdp"_ls).toString(); - } -}; -} // namespace Quotient diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 2843e1dc..ca751081 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -3,6 +3,7 @@ #include "event.h" +#include "callevents.h" #include "logging.h" #include "stateevent.h" @@ -74,7 +75,7 @@ const QJsonObject Event::unsignedJson() const bool Event::isStateEvent() const { return is(); } -bool Event::isCallEvent() const { return is(); } +bool Event::isCallEvent() const { return is(); } void Event::dumpTo(QDebug dbg) const { diff --git a/lib/events/event.h b/lib/events/event.h index ea5a2554..6a7acf28 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -361,7 +361,7 @@ public: // as an exception. For other base events, Event::is<>() and // Quotient::is<>() should be used; don't add is* methods here bool isStateEvent() const; - [[deprecated("Use is() instead")]] bool isCallEvent() const; + [[deprecated("Use is() instead")]] bool isCallEvent() const; protected: QJsonObject& editJson() { return _json; } @@ -386,7 +386,7 @@ using Events = EventsArray; //! don't need to create an event from its content structure, just go and derive //! straight from the respective \p EventBaseT instead of using EventTemplate); //! specialisations may override that and provide useful semantics even without -//! \p ContentT (see EventTemplate, e.g.). +//! \p ContentT (see EventTemplate, e.g.). //! //! The template uses CRTP to pick the event type id from the actual class; //! it will fail to compile if \p EventT doesn't provide TypeId. It also uses @@ -402,8 +402,8 @@ public: !std::is_same_v, "If you see this, you tried to use EventTemplate with the default" " ContentT type, which is void. This default is only used with explicit" - " specialisations (see CallEventBase, e.g.). Otherwise, if you don't" - " intend to use the content part of EventTemplate then you don't need" + " specialisations (see CallEvent, e.g.). Otherwise, if you don't intend" + " to use the content part of EventTemplate then you don't need" " EventTemplate; just use the base event class directly"); using content_type = ContentT; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index bd06f5c5..8928c81c 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -96,22 +96,6 @@ void RoomEvent::dumpTo(QDebug dbg) const dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; } -QJsonObject CallEventBase::basicJson(const QString& matrixType, - const QString& callId, int version, - QJsonObject contentJson) -{ - contentJson.insert(QStringLiteral("call_id"), callId); - contentJson.insert(QStringLiteral("version"), version); - return RoomEvent::basicJson(matrixType, contentJson); -} - -CallEventBase::CallEventBase(const QJsonObject& json) - : RoomEvent(json) -{ - if (callId().isEmpty()) - qCWarning(EVENTS) << id() << "is a call event with an empty call id"; -} - #ifdef Quotient_E2EE_ENABLED void RoomEvent::setOriginalEvent(event_ptr_tt&& originalEvent) { diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 830f1d30..47b0b59d 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -79,31 +79,6 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = Range; -class QUOTIENT_API CallEventBase : public RoomEvent { -public: - QUO_BASE_EVENT(CallEventBase, "m.call.*"_ls, RoomEvent::BaseMetaType) - - explicit CallEventBase(const QJsonObject& json); - - QUO_CONTENT_GETTER(QString, callId) - QUO_CONTENT_GETTER(int, version) - -protected: - static QJsonObject basicJson(const QString& matrixType, - const QString& callId, int version, - QJsonObject contentJson = {}); -}; - -template -class EventTemplate : public CallEventBase { -public: - using CallEventBase::CallEventBase; - explicit EventTemplate(const QString& callId, - const QJsonObject& contentJson = {}) - : EventTemplate(basicJson(EventT::TypeId, callId, 0, contentJson)) - {} -}; - } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) Q_DECLARE_METATYPE(const Quotient::RoomEvent*) 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 575534e7cca310c6d6195ab16d482bf9dfba755e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 1 Aug 2022 18:09:35 +0200 Subject: Disallow direct events construction from JSON Direct construction (using makeEvent() or explicitly constructing an event) from JSON may create an event that has a type conflicting with that stored in JSON. There's no such problem with loadEvent(), even though it's considerably slower. Driven by the fact that almost nowhere in the code direct construction is used on checked JSON (one test is the only valid case), this commit moves all JSON-loading constructors to the protected section, thereby disabling usage of makeEvent() in JSON-loading capacity, and switches such cases across the library to loadEvent(). --- autotests/callcandidateseventtest.cpp | 13 ++++++++----- lib/connection.cpp | 10 ++++++---- lib/events/callevents.h | 4 ++-- lib/events/event.h | 11 +++++++++-- lib/events/roomevent.cpp | 2 +- lib/events/roomevent.h | 8 ++++---- lib/events/stateevent.h | 15 +++++++++------ 7 files changed, 39 insertions(+), 24 deletions(-) diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index b37dd109..257e0ef2 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -40,13 +40,16 @@ void TestCallCandidatesEvent::fromJson() auto object = document.object(); - Quotient::CallCandidatesEvent callCandidatesEvent(object); + using namespace Quotient; + const auto& callCandidatesEvent = loadEvent(object); + QVERIFY(callCandidatesEvent); + QVERIFY(callCandidatesEvent->is()); - QCOMPARE(callCandidatesEvent.version(), 0); - QCOMPARE(callCandidatesEvent.callId(), QStringLiteral("12345")); - QCOMPARE(callCandidatesEvent.candidates().count(), 1); + QCOMPARE(callCandidatesEvent->version(), 0); + QCOMPARE(callCandidatesEvent->callId(), QStringLiteral("12345")); + QCOMPARE(callCandidatesEvent->candidates().count(), 1); - const QJsonObject &candidate = callCandidatesEvent.candidates().at(0).toObject(); + const auto& candidate = callCandidatesEvent->candidates().at(0).toObject(); QCOMPARE(candidate.value("sdpMid").toString(), QStringLiteral("audio")); QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0); QCOMPARE(candidate.value("candidate").toString(), diff --git a/lib/connection.cpp b/lib/connection.cpp index 471dc20d..a33ace51 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2242,10 +2242,12 @@ void Connection::saveOlmAccount() #ifdef Quotient_E2EE_ENABLED QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) { - auto r = room(notification["room_id"].toString()); - auto event = makeEvent(notification["event"].toObject()); - const auto decrypted = r->decryptMessage(*event); - return decrypted ? decrypted->fullJson() : QJsonObject(); + if (auto r = room(notification["room_id"].toString())) + if (auto event = + loadEvent(notification["event"].toObject())) + if (const auto decrypted = r->decryptMessage(*event)) + return decrypted->fullJson(); + return QJsonObject(); } Database* Connection::database() const diff --git a/lib/events/callevents.h b/lib/events/callevents.h index 6d9feae4..752e331d 100644 --- a/lib/events/callevents.h +++ b/lib/events/callevents.h @@ -15,12 +15,12 @@ public: return mType.startsWith("m.call."); } - explicit CallEvent(const QJsonObject& json); - QUO_CONTENT_GETTER(QString, callId) QUO_CONTENT_GETTER(int, version) protected: + explicit CallEvent(const QJsonObject& json); + static QJsonObject basicJson(const QString& matrixType, const QString& callId, int version, QJsonObject contentJson = {}); diff --git a/lib/events/event.h b/lib/events/event.h index 6a7acf28..9d7c61a9 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -203,6 +203,9 @@ private: // === Event creation facilities === //! \brief Create an event of arbitrary type from its arguments +//! +//! This should not be used to load events from JSON - use loadEvent() for that. +//! \sa loadEvent template inline event_ptr_tt makeEvent(ArgTs&&... args) { @@ -266,8 +269,6 @@ public: return BaseMetaType; } - explicit Event(const QJsonObject& json); - Q_DISABLE_COPY(Event) Event(Event&&) noexcept = default; Event& operator=(Event&&) = delete; @@ -364,6 +365,10 @@ public: [[deprecated("Use is() instead")]] bool isCallEvent() const; protected: + friend class EventMetaType; // To access the below constructor + + explicit Event(const QJsonObject& json); + QJsonObject& editJson() { return _json; } virtual void dumpTo(QDebug dbg) const; @@ -427,6 +432,7 @@ public: //! pointing to that BaseMetaType. //! \sa EventMetaType, EventMetaType::SuppressLoadDerived #define QUO_BASE_EVENT(CppType_, ...) \ + friend class EventMetaType; \ static inline EventMetaType BaseMetaType{ \ #CppType_ __VA_OPT__(,) __VA_ARGS__ }; \ const AbstractEventMetaType& metaType() const override \ @@ -452,6 +458,7 @@ public: //! \sa EventMetaType #define QUO_EVENT(CppType_, MatrixType_, ...) \ static inline const auto& TypeId = MatrixType_##_ls; \ + friend class EventMetaType; \ static inline const EventMetaType MetaType{ \ #CppType_, TypeId, BaseMetaType __VA_OPT__(,) __VA_ARGS__ \ }; \ diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 8928c81c..e98cb591 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -12,7 +12,7 @@ RoomEvent::RoomEvent(const QJsonObject& json) : Event(json) { if (const auto redaction = unsignedPart(RedactedCauseKeyL); !redaction.isEmpty()) - _redactedBecause = makeEvent(redaction); + _redactedBecause = loadEvent(redaction); } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 47b0b59d..203434f6 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -16,10 +16,7 @@ class QUOTIENT_API RoomEvent : public Event { public: QUO_BASE_EVENT(RoomEvent, {}, Event::BaseMetaType) - // RedactionEvent is an incomplete type here so we cannot inline - // constructors using it and also destructors (with 'using', in particular). - explicit RoomEvent(const QJsonObject& json); - ~RoomEvent() override; + ~RoomEvent() override; // Don't inline this - see the private section QString id() const; QDateTime originTimestamp() const; @@ -66,9 +63,12 @@ public: #endif protected: + explicit RoomEvent(const QJsonObject& json); void dumpTo(QDebug dbg) const override; private: + // RedactionEvent is an incomplete type here so we cannot inline + // constructors using it and also destructors (with 'using', in particular). event_ptr_tt _redactedBecause; #ifdef Quotient_E2EE_ENABLED diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 911972f2..ffbce76e 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -24,7 +24,6 @@ public: //! constructors and calls in, e.g., RoomStateView don't include it. static constexpr auto needsStateKey = false; - explicit StateEventBase(const QJsonObject& json); explicit StateEventBase(Type type, const QString& stateKey = {}, const QJsonObject& contentJson = {}); @@ -39,9 +38,11 @@ public: } QString replacedState() const; - void dumpTo(QDebug dbg) const override; - virtual bool repeatsState() const; + +protected: + explicit StateEventBase(const QJsonObject& json); + void dumpTo(QDebug dbg) const override; }; using StateEventPtr = event_ptr_tt; using StateEvents = EventsArray; @@ -129,13 +130,15 @@ private: using base_type = EventTemplate; public: - explicit KeylessStateEventBase(const QJsonObject& fullJson) - : base_type(fullJson) - {} template explicit KeylessStateEventBase(ContentParamTs&&... contentParams) : base_type(QString(), std::forward(contentParams)...) {} + +protected: + explicit KeylessStateEventBase(const QJsonObject& fullJson) + : base_type(fullJson) + {} }; template -- cgit v1.2.3 From bde38f86337d6f49b34b38016ab088d2f48ec371 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 1 Aug 2022 18:09:47 +0200 Subject: concept EventClass Constrain types to derive from Event (or the chosen class), where applicable. --- lib/connection.cpp | 2 +- lib/connection.h | 2 +- lib/eventitem.h | 4 ++-- lib/events/event.h | 43 ++++++++++++++++++++++++++----------------- quotest/quotest.cpp | 4 ++-- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a33ace51..d9268028 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -188,7 +188,7 @@ public: emit q->accountDataChanged(eventType); } - template + template void packAndSendAccountData(ContentT&& content) { packAndSendAccountData( diff --git a/lib/connection.h b/lib/connection.h index 66ed8b68..5afcfc2f 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -189,7 +189,7 @@ public: //! of that type. //! \note Direct chats map cannot be retrieved using this method _yet_; //! use directChats() instead. - template + template const EventT* accountData() const { return eventCast(accountData(EventT::TypeId)); diff --git a/lib/eventitem.h b/lib/eventitem.h index 2e55a724..445f8265 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -46,7 +46,7 @@ public: const RoomEvent* event() const { return rawPtr(evt); } const RoomEvent* get() const { return event(); } - template + template EventT> const EventT* viewAs() const { return eventCast(evt); @@ -67,7 +67,7 @@ public: std::any& userData() { return data; } protected: - template + template EventT> EventT* getAs() { return eventCast(evt); diff --git a/lib/events/event.h b/lib/events/event.h index 9d7c61a9..d0b63085 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -64,7 +64,11 @@ struct QUOTIENT_API EventTypeRegistry { class Event; -template +// TODO: move over to std::derived_from once it's available everywhere +template +concept EventClass = std::is_base_of_v; + +template bool is(const Event& e); //! \brief The base class for event metatypes @@ -206,13 +210,13 @@ private: //! //! This should not be used to load events from JSON - use loadEvent() for that. //! \sa loadEvent -template +template inline event_ptr_tt makeEvent(ArgTs&&... args) { return std::make_unique(std::forward(args)...); } -template +template constexpr const auto& mostSpecificMetaType() { if constexpr (requires { EventT::MetaType; }) @@ -226,7 +230,7 @@ constexpr const auto& mostSpecificMetaType() //! Use this factory template to detect the type from the JSON object //! contents (the detected event type should derive from the template //! parameter type) and create an event object of that type. -template +template inline event_ptr_tt loadEvent(const QJsonObject& fullJson) { return mostSpecificMetaType().loadFrom( @@ -238,7 +242,7 @@ inline event_ptr_tt loadEvent(const QJsonObject& fullJson) //! Use this template to resolve the C++ type from the Matrix type string in //! \p matrixType and create an event of that type by passing all parameters //! to BaseEventT::basicJson(). -template +template inline event_ptr_tt loadEvent(const QString& matrixType, const auto&... otherBasicJsonParams) { @@ -246,7 +250,7 @@ inline event_ptr_tt loadEvent(const QString& matrixType, EventT::basicJson(matrixType, otherBasicJsonParams...), matrixType); } -template +template struct JsonConverter> : JsonObjectUnpacker> { // No dump() to avoid any ambiguity on whether a given export to JSON uses @@ -295,7 +299,7 @@ public: //! the returned value will be different. QString matrixType() const; - template + template bool is() const { return Quotient::is(*this); @@ -377,7 +381,7 @@ private: }; using EventPtr = event_ptr_tt; -template +template using EventsArray = std::vector>; using Events = EventsArray; @@ -400,8 +404,10 @@ using Events = EventsArray; //! your class will likely be clearer and more concise. //! \sa https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern //! \sa DEFINE_SIMPLE_EVENT -template +template class EventTemplate : public BaseEventT { + // Above: can't constrain EventT to be EventClass because it's incomplete + // by CRTP definition. public: static_assert( !std::is_same_v, @@ -522,7 +528,7 @@ public: // === is<>(), eventCast<>() and switchOnType<>() === -template +template inline bool is(const Event& e) { if constexpr (requires { EventT::MetaType; }) { @@ -544,7 +550,7 @@ inline bool is(const Event& e) //! can be either "dumb" (BaseEventT*) or "smart" (`event_ptr_tt<>`). This //! overload doesn't affect the event ownership - if the original pointer owns //! the event it must outlive the downcast pointer to keep it from dangling. -template +template inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast(&*eptr)) { @@ -567,7 +573,7 @@ inline auto eventCast(const BasePtrT& eptr) //! after calling this overload; if it is a temporary, this normally //! leads to the event getting deleted along with the end of //! the temporary's lifetime. -template +template inline auto eventCast(event_ptr_tt&& eptr) { return eptr && is>(*eptr) @@ -576,12 +582,15 @@ inline auto eventCast(event_ptr_tt&& eptr) } namespace _impl { - template - concept Invocable_With_Downcast = + template + concept Invocable_With_Downcast = requires + { + requires EventClass; std::is_base_of_v>>; + }; } -template +template inline auto switchOnType(const BaseT& event, TailT&& tail) { if constexpr (std::is_invocable_v) { @@ -596,7 +605,7 @@ inline auto switchOnType(const BaseT& event, TailT&& tail) } } -template +template inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) { using event_type1 = fn_arg_t; @@ -605,7 +614,7 @@ inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) return switchOnType(event, std::forward(fns)...); } -template +template [[deprecated("The new name for visit() is switchOnType()")]] // inline auto visit(const BaseT& event, FnTs&&... fns) { diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index 3ac6404a..624888be 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -128,7 +128,7 @@ private: [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, const QString& evtIdToRedact); - template + template EventT> [[nodiscard]] bool validatePendingEvent(const QString& txnId); [[nodiscard]] bool checkDirectChat() const; void finishTest(const TestToken& token, bool condition, const char* file, @@ -156,7 +156,7 @@ void TestSuite::doTest(const QByteArray& testName) Q_ARG(TestToken, testName)); } -template +template EventT> bool TestSuite::validatePendingEvent(const QString& txnId) { auto it = targetRoom->findPendingEvent(txnId); -- 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/eventitem.h | 5 ++--- lib/events/event.cpp | 2 +- lib/events/event.h | 4 ++-- lib/events/roomcreateevent.h | 4 ++-- lib/events/roomtombstoneevent.h | 4 ++-- lib/events/stateevent.cpp | 13 ++++++------- lib/events/stateevent.h | 42 +++++++++++++++++++++-------------------- lib/room.cpp | 15 +++++++-------- lib/room.h | 20 ++++++++++++-------- lib/roomstateview.cpp | 10 +++++----- lib/roomstateview.h | 13 ++++++------- 11 files changed, 67 insertions(+), 65 deletions(-) diff --git a/lib/eventitem.h b/lib/eventitem.h index 445f8265..96e45b38 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -95,10 +95,9 @@ private: }; template <> -inline const StateEventBase* EventItemBase::viewAs() const +inline const StateEvent* EventItemBase::viewAs() const { - return evt->isStateEvent() ? weakPtrCast(evt) - : nullptr; + return evt->isStateEvent() ? weakPtrCast(evt) : nullptr; } template <> diff --git a/lib/events/event.cpp b/lib/events/event.cpp index ca751081..da7de919 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -73,7 +73,7 @@ const QJsonObject Event::unsignedJson() const return fullJson()[UnsignedKeyL].toObject(); } -bool Event::isStateEvent() const { return is(); } +bool Event::isStateEvent() const { return is(); } bool Event::isCallEvent() const { return is(); } diff --git a/lib/events/event.h b/lib/events/event.h index d0b63085..c8ef5acb 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -125,7 +125,7 @@ inline bool operator==(const AbstractEventMetaType& lhs, //! //! TL;DR for the loadFrom() story: //! - for base event types, use QUO_BASE_EVENT and, if you have additional -//! validation (e.g., JSON has to contain a certain key - see StateEventBase +//! validation (e.g., JSON has to contain a certain key - see StateEvent //! for a real example), define it in the static EventT::isValid() member //! function accepting QJsonObject and returning bool. //! - for leaf (specific) event types - simply use QUO_EVENT and it will do @@ -153,7 +153,7 @@ public: //! any of its base event types) has a static isValid() predicate and //! the event JSON does not satisfy it, nullptr is immediately returned //! to the upper level or to the loadFrom() caller. This is how existence - //! of `state_key` is checked in any type derived from StateEventBase. + //! of `state_key` is checked in any type derived from StateEvent. //! 3. If step 1b above returned non-nullptr, immediately return it. //! 4. //! a. If EventT::isValid() or EventT::TypeId (either, or both) exist and diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 2709258f..5968e187 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -7,11 +7,11 @@ #include "quotient_common.h" namespace Quotient { -class QUOTIENT_API RoomCreateEvent : public StateEventBase { +class QUOTIENT_API RoomCreateEvent : public StateEvent { public: QUO_EVENT(RoomCreateEvent, "m.room.create") - using StateEventBase::StateEventBase; + using StateEvent::StateEvent; struct Predecessor { QString roomId; diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 95743e32..c85b4dfd 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -6,11 +6,11 @@ #include "stateevent.h" namespace Quotient { -class QUOTIENT_API RoomTombstoneEvent : public StateEventBase { +class QUOTIENT_API RoomTombstoneEvent : public StateEvent { public: QUO_EVENT(RoomTombstoneEvent, "m.room.tombstone") - using StateEventBase::StateEventBase; + using StateEvent::StateEvent; QString serverMessage() const; QString successorRoomId() const; diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 204044bb..72ecd5ad 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -6,30 +6,29 @@ using namespace Quotient; -StateEventBase::StateEventBase(const QJsonObject& json) +StateEvent::StateEvent(const QJsonObject& json) : RoomEvent(json) { Q_ASSERT_X(json.contains(StateKeyKeyL), __FUNCTION__, "Attempt to create a state event without state key"); } -StateEventBase::StateEventBase(Event::Type type, const QString& stateKey, +StateEvent::StateEvent(Event::Type type, const QString& stateKey, const QJsonObject& contentJson) : RoomEvent(basicJson(type, stateKey, contentJson)) {} -bool StateEventBase::repeatsState() const +bool StateEvent::repeatsState() const { - const auto prevContentJson = unsignedPart(PrevContentKeyL); - return fullJson().value(ContentKeyL) == prevContentJson; + return contentJson() == unsignedPart(PrevContentKeyL); } -QString StateEventBase::replacedState() const +QString StateEvent::replacedState() const { return unsignedPart("replaces_state"_ls); } -void StateEventBase::dumpTo(QDebug dbg) const +void StateEvent::dumpTo(QDebug dbg) const { if (!stateKey().isEmpty()) dbg << '<' << stateKey() << "> "; diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index ffbce76e..992ec2e2 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -7,10 +7,10 @@ namespace Quotient { -class QUOTIENT_API StateEventBase : public RoomEvent { +class QUOTIENT_API StateEvent : public RoomEvent { public: - QUO_BASE_EVENT(StateEventBase, "json.contains('state_key')"_ls, - RoomEvent::BaseMetaType) + QUO_BASE_EVENT(StateEvent, "json.contains('state_key')"_ls, + RoomEvent::BaseMetaType) static bool isValid(const QJsonObject& fullJson) { return fullJson.contains(StateKeyKeyL); @@ -24,8 +24,8 @@ public: //! constructors and calls in, e.g., RoomStateView don't include it. static constexpr auto needsStateKey = false; - explicit StateEventBase(Type type, const QString& stateKey = {}, - const QJsonObject& contentJson = {}); + explicit StateEvent(Type type, const QString& stateKey = {}, + const QJsonObject& contentJson = {}); //! Make a minimal correct Matrix state event JSON static QJsonObject basicJson(const QString& matrixTypeId, @@ -41,18 +41,20 @@ public: virtual bool repeatsState() const; protected: - explicit StateEventBase(const QJsonObject& json); + explicit StateEvent(const QJsonObject& json); void dumpTo(QDebug dbg) const override; }; -using StateEventPtr = event_ptr_tt; -using StateEvents = EventsArray; +using StateEventBase + [[deprecated("StateEventBase is StateEvent now")]] = StateEvent; +using StateEventPtr = event_ptr_tt; +using StateEvents = EventsArray; -[[deprecated("Use StateEventBase::basicJson() instead")]] +[[deprecated("Use StateEvent::basicJson() instead")]] inline QJsonObject basicStateEventJson(const QString& matrixTypeId, const QJsonObject& content, const QString& stateKey = {}) { - return StateEventBase::basicJson(matrixTypeId, stateKey, content); + return StateEvent::basicJson(matrixTypeId, stateKey, content); } /** @@ -64,8 +66,8 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, using StateEventKey = std::pair; template -class EventTemplate - : public StateEventBase { +class EventTemplate + : public StateEvent { public: using content_type = ContentT; @@ -82,14 +84,14 @@ public: }; explicit EventTemplate(const QJsonObject& fullJson) - : StateEventBase(fullJson) + : StateEvent(fullJson) , _content(fromJson(Event::contentJson())) , _prev(unsignedJson()) {} template explicit EventTemplate(const QString& stateKey, ContentParamTs&&... contentParams) - : StateEventBase(EventT::TypeId, stateKey) + : StateEvent(EventT::TypeId, stateKey) , _content { std::forward(contentParams)... } { editJson().insert(ContentKey, toJson(_content)); @@ -113,11 +115,11 @@ private: template class KeyedStateEventBase - : public EventTemplate { + : public EventTemplate { public: static constexpr auto needsStateKey = true; - using EventTemplate::EventTemplate; + using EventTemplate::EventTemplate; }; template @@ -125,9 +127,9 @@ concept Keyed_State_Event = EvT::needsStateKey; template class KeylessStateEventBase - : public EventTemplate { + : public EventTemplate { private: - using base_type = EventTemplate; + using base_type = EventTemplate; public: template @@ -145,5 +147,5 @@ template concept Keyless_State_Event = !EvT::needsStateKey; } // namespace Quotient -Q_DECLARE_METATYPE(Quotient::StateEventBase*) -Q_DECLARE_METATYPE(const Quotient::StateEventBase*) +Q_DECLARE_METATYPE(Quotient::StateEvent*) +Q_DECLARE_METATYPE(const Quotient::StateEvent*) 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())); diff --git a/lib/room.h b/lib/room.h index b454bfbc..5eb66b8b 100644 --- a/lib/room.h +++ b/lib/room.h @@ -757,9 +757,8 @@ public: */ [[deprecated("Use currentState().get() instead; " "make sure to check its result for nullptrs")]] // - const Quotient::StateEventBase* - getCurrentState(const QString& evtType, - const QString& stateKey = {}) const; + const StateEvent* getCurrentState(const QString& evtType, + const QString& stateKey = {}) const; /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of @@ -782,16 +781,21 @@ public: RoomStateView currentState() const; //! Send a request to update the room state with the given event - SetRoomStateWithKeyJob* setState(const StateEventBase& evt); + SetRoomStateWithKeyJob* setState(const StateEvent& evt); //! \brief Set a state event of the given type with the given arguments //! //! This typesafe overload attempts to send a state event with the type //! \p EvT and the content defined by \p args. Specifically, the function - //! creates a temporary object of type \p EvT passing \p args to - //! the constructor, and sends a request to the homeserver using - //! the Matrix event type defined by \p EvT and the event content produced - //! via EvT::contentJson(). + //! constructs a temporary object of type \p EvT with its content + //! list-initialised from \p args, and sends a request to the homeserver + //! using the Matrix event type defined by \p EvT and the event content + //! produced via EvT::contentJson(). + //! + //! \note This call is not suitable for events that assume non-empty + //! stateKey, such as member events; for those you have to create + //! a temporary event object yourself and use the setState() overload + //! that accepts StateEvent const-ref. template auto setState(ArgTs&&... args) { diff --git a/lib/roomstateview.cpp b/lib/roomstateview.cpp index 94c88eee..be0f7c6c 100644 --- a/lib/roomstateview.cpp +++ b/lib/roomstateview.cpp @@ -5,8 +5,8 @@ using namespace Quotient; -const StateEventBase* RoomStateView::get(const QString& evtType, - const QString& stateKey) const +const StateEvent* RoomStateView::get(const QString& evtType, + const QString& stateKey) const { return value({ evtType, stateKey }); } @@ -23,10 +23,10 @@ QJsonObject RoomStateView::contentJson(const QString& evtType, return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject()); } -const QVector -RoomStateView::eventsOfType(const QString& evtType) const +const QVector RoomStateView::eventsOfType( + const QString& evtType) const { - auto vals = QVector(); + auto vals = QVector(); for (auto it = cbegin(); it != cend(); ++it) if (it.key().first == evtType) vals.append(it.value()); diff --git a/lib/roomstateview.h b/lib/roomstateview.h index 119c24cf..c5261a1e 100644 --- a/lib/roomstateview.h +++ b/lib/roomstateview.h @@ -22,10 +22,10 @@ template >> concept Keyless_State_Fn = !EvT::needsStateKey; class QUOTIENT_API RoomStateView - : private QHash { + : private QHash { Q_GADGET public: - const QHash& events() const + const QHash& events() const { return *this; } @@ -40,8 +40,8 @@ public: //! have to check that it has_value() before using. Alternatively //! you can now use queryCurrentState() to access state safely. //! \sa getCurrentStateContentJson - const StateEventBase* get(const QString& evtType, - const QString& stateKey = {}) const; + const StateEvent* get(const QString& evtType, + const QString& stateKey = {}) const; //! \brief Get a state event with the given event type and state key //! @@ -94,7 +94,7 @@ public: auto content(const QString& stateKey, typename EvT::content_type defaultValue = {}) const { - // StateEvent<>::content is special in that it returns a const-ref, + // EventBase<>::content is special in that it returns a const-ref, // and lift() inside queryOr() can't wrap that in a temporary Omittable. if (const auto evt = get(stateKey)) return evt->content(); @@ -122,8 +122,7 @@ public: //! //! This method returns all known state events that have occured in //! the room of the given type. - const QVector - eventsOfType(const QString& evtType) const; + const QVector eventsOfType(const QString& evtType) const; //! \brief Run a function on a state event with the given type and key //! -- 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(-) 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 bd2736bc9f8b6023ecbc21d0d831856703b853db Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 21 Jun 2022 07:37:01 +0200 Subject: SingleKeyValue: use reference for template parameter I guess it was simply overlooked originally; in any case, currently used compilers deal with the reference just as fine as with the pointer. --- lib/events/event.h | 26 +++++++++++++------------- lib/events/simplestateevents.h | 24 ++++++++++++------------ lib/events/single_key_value.h | 6 +++--- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index c8ef5acb..0abef1f0 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -511,19 +511,19 @@ public: /// To retrieve the value the getter uses a JSON key name that corresponds to /// its own (getter's) name but written in snake_case. \p GetterName_ must be /// in camelCase, no quotes (an identifier, not a literal). -#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ - JsonKey_) \ - constexpr auto Name_##ContentKey = JsonKey_##_ls; \ - class QUOTIENT_API Name_ \ - : public EventTemplate< \ - Name_, Base_, \ - EventContent::SingleKeyValue> { \ - public: \ - QUO_EVENT(Name_, TypeId_) \ - using value_type = ValueType_; \ - using EventTemplate::EventTemplate; \ - QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey) \ - }; \ +#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ + JsonKey_) \ + constexpr auto Name_##ContentKey = JsonKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public EventTemplate< \ + Name_, Base_, \ + EventContent::SingleKeyValue> { \ + public: \ + QUO_EVENT(Name_, TypeId_) \ + using value_type = ValueType_; \ + using EventTemplate::EventTemplate; \ + QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey) \ + }; \ // End of macro // === is<>(), eventCast<>() and switchOnType<>() === diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index d84dc1b1..2a0d3817 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -7,17 +7,17 @@ #include "single_key_value.h" namespace Quotient { -#define DEFINE_SIMPLE_STATE_EVENT(Name_, TypeId_, ValueType_, ContentKey_) \ - constexpr auto Name_##Key = #ContentKey_##_ls; \ - class QUOTIENT_API Name_ \ - : public KeylessStateEventBase< \ - Name_, EventContent::SingleKeyValue> { \ - public: \ - using value_type = ValueType_; \ - QUO_EVENT(Name_, TypeId_) \ - using KeylessStateEventBase::KeylessStateEventBase; \ - auto ContentKey_() const { return content().value; } \ - }; \ +#define DEFINE_SIMPLE_STATE_EVENT(Name_, TypeId_, ValueType_, ContentKey_) \ + constexpr auto Name_##Key = #ContentKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public KeylessStateEventBase< \ + Name_, EventContent::SingleKeyValue> { \ + public: \ + using value_type = ValueType_; \ + QUO_EVENT(Name_, TypeId_) \ + using KeylessStateEventBase::KeylessStateEventBase; \ + auto ContentKey_() const { return content().value; } \ + }; \ // End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) @@ -29,7 +29,7 @@ constexpr auto RoomAliasesEventKey = "aliases"_ls; class QUOTIENT_API RoomAliasesEvent : public KeyedStateEventBase< RoomAliasesEvent, - EventContent::SingleKeyValue> + EventContent::SingleKeyValue> { public: QUO_EVENT(RoomAliasesEvent, "m.room.aliases") diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h index 5edff3b1..ca2bd331 100644 --- a/lib/events/single_key_value.h +++ b/lib/events/single_key_value.h @@ -5,7 +5,7 @@ namespace Quotient { namespace EventContent { - template + template struct SingleKeyValue { // NOLINTBEGIN(google-explicit-constructor): that check should learn // about explicit(false) @@ -20,7 +20,7 @@ namespace EventContent { }; } // namespace EventContent -template +template struct JsonConverter> { using content_type = EventContent::SingleKeyValue; static content_type load(const QJsonValue& jv) @@ -31,6 +31,6 @@ struct JsonConverter> { { return { { JsonKey, toJson(c.value) } }; } - static inline const auto JsonKey = toSnakeCase(*KeyStr); + static inline const auto JsonKey = toSnakeCase(KeyStr); }; } // namespace Quotient -- cgit v1.2.3 From 66127730592eadf9ee717a53a521ac2ec14f1051 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 5 Sep 2022 15:20:58 +0200 Subject: sendToDevice: fix unintended slicing Ironically, this slicing would not break anything as all the necessary data are saved in the Event parent class; but the code is very fragile and scary. --- lib/connection.cpp | 2 +- lib/connection.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index d9268028..53c99969 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2471,7 +2471,7 @@ void Connection::startKeyVerificationSession(const QString& deviceId) } void Connection::sendToDevice(const QString& targetUserId, - const QString& targetDeviceId, Event event, + const QString& targetDeviceId, const Event& event, bool encrypted) { const auto contentJson = diff --git a/lib/connection.h b/lib/connection.h index 5afcfc2f..79c23c9a 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -336,7 +336,7 @@ public: // This assumes that an olm session already exists. If it doesn't, no message is sent. void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, - Event event, bool encrypted); + const Event& event, bool encrypted); /// Returns true if this megolm session comes from a verified device bool isVerifiedSession(const QString& megolmSessionId) const; -- cgit v1.2.3 From 16350ce5c6fc020474c6980e3e72ab369e8b33f4 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 5 Sep 2022 16:51:15 +0200 Subject: Cleanup --- lib/keyverificationsession.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 44a085fe..c6b62a83 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -306,9 +306,10 @@ void KeyVerificationSession::sendStartSas() { startSentByUs = true; KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); - m_startEvent = QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, - std::move(event), m_encrypted); + m_startEvent = + QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, event, + m_encrypted); setState(WAITINGFORACCEPT); } -- cgit v1.2.3 From ecf6b855b0fc8cbe16d34b67ae1dca7bd9b69948 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 6 Sep 2022 21:35:27 +0200 Subject: Add autotest for key verification and fix several edge-cases --- autotests/CMakeLists.txt | 1 + autotests/testkeyverification.cpp | 50 +++++++++++++++++++++++++++++++++++++++ autotests/testkeyverification.h | 18 ++++++++++++++ autotests/testolmaccount.cpp | 22 ++--------------- autotests/testutils.h | 32 +++++++++++++++++++++++++ lib/keyverificationsession.cpp | 15 +++++++++--- lib/keyverificationsession.h | 7 ++++++ 7 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 autotests/testkeyverification.cpp create mode 100644 autotests/testkeyverification.h create mode 100644 autotests/testutils.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index c11901bf..48edb168 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -19,4 +19,5 @@ if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testolmsession) quotient_add_test(NAME testolmutility) quotient_add_test(NAME testfilecrypto) + quotient_add_test(NAME testkeyverification) endif() diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp new file mode 100644 index 00000000..fbbb7614 --- /dev/null +++ b/autotests/testkeyverification.cpp @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testkeyverification.h" +#include "qt_connection_util.h" + +void TestKeyVerificationSession::testVerification() +{ + CREATE_CONNECTION(a, "alice1", "secret", "AliceDesktop") + CREATE_CONNECTION(b, "alice1", "secret", "AlicePhone") + + KeyVerificationSession *aSession = nullptr; + connect(a.get(), &Connection::newKeyVerificationSession, this, [&](KeyVerificationSession* session) { + aSession = session; + QVERIFY(session->remoteDeviceId() == b->deviceId()); + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::ACCEPTED); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); + session->sendMac(); + }); + }); + }); + a->startKeyVerificationSession(b->deviceId()); + connect(b.get(), &Connection::newKeyVerificationSession, this, [=](KeyVerificationSession* session) { + QVERIFY(session->remoteDeviceId() == a->deviceId()); + QVERIFY(session->state() == KeyVerificationSession::INCOMING); + session->sendReady(); + // KeyVerificationSession::READY is skipped because we have only one method + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORKEY || session->state() == KeyVerificationSession::ACCEPTED); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=]() { + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); + QVERIFY(aSession); + QVERIFY(aSession->sasEmojis() == session->sasEmojis()); + session->sendMac(); + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORMAC); + }); + }); + + }); + b->syncLoop(); + a->syncLoop(); + QSignalSpy spy(b.get(), &Connection::incomingKeyVerificationDone); + spy.wait(10000); +} +QTEST_GUILESS_MAIN(TestKeyVerificationSession) diff --git a/autotests/testkeyverification.h b/autotests/testkeyverification.h new file mode 100644 index 00000000..28e00e11 --- /dev/null +++ b/autotests/testkeyverification.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include "testutils.h" + +class TestKeyVerificationSession : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testVerification(); + +}; + diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 4b32393d..280705d0 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -14,6 +14,8 @@ #include #include +#include "testutils.h" + using namespace Quotient; void TestOlmAccount::pickleUnpickledTest() @@ -168,26 +170,6 @@ void TestOlmAccount::encryptedFile() QCOMPARE(file.key.kty, "oct"); } -#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ - NetworkAccessManager::instance()->ignoreSslErrors(true); \ - auto VAR = std::make_shared(); \ - (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \ - connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \ - (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \ - }); \ - connect((VAR).get(), &Connection::networkError, [](const QString& error) { \ - QWARN(qUtf8Printable(error)); \ - QFAIL("Network error: make sure synapse is running"); \ - }); \ - connect((VAR).get(), &Connection::loginError, [](const QString& error) { \ - QWARN(qUtf8Printable(error)); \ - QFAIL("Login failed"); \ - }); \ - QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \ - QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \ - QVERIFY(spy##VAR.wait(10000)); \ - QVERIFY(spy2##VAR.wait(10000)); - void TestOlmAccount::uploadIdentityKey() { CREATE_CONNECTION(conn, "alice1", "secret", "AlicePhone") diff --git a/autotests/testutils.h b/autotests/testutils.h new file mode 100644 index 00000000..7d016a34 --- /dev/null +++ b/autotests/testutils.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +#include + +using namespace Quotient; + +#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ + NetworkAccessManager::instance()->ignoreSslErrors(true); \ + auto VAR = std::make_shared(); \ + (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \ + connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \ + (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \ + }); \ + connect((VAR).get(), &Connection::networkError, [](const QString& error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Network error: make sure synapse is running"); \ + }); \ + connect((VAR).get(), &Connection::loginError, [](const QString& error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Login failed"); \ + }); \ + QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \ + QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \ + QVERIFY(spy##VAR.wait(10000)); \ + QVERIFY(spy2##VAR.wait(10000)); diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 44a085fe..24fc08e1 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -154,7 +154,7 @@ EmojiEntry emojiForCode(int code, const QString& language) void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) { - if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { + if (state() != WAITINGFORKEY && state() != ACCEPTED) { cancelVerification(UNEXPECTED_MESSAGE); return; } @@ -176,7 +176,6 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) } else { sendKey(); } - setState(WAITINGFORVERIFICATION); std::string key(olm_sas_pubkey_length(m_sas), '\0'); olm_sas_get_pubkey(m_sas, key.data(), key.size()); @@ -214,6 +213,7 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) emit sasEmojisChanged(); emit keyReceived(); + setState(WAITINGFORVERIFICATION); } QString KeyVerificationSession::calculateMac(const QString& input, @@ -314,6 +314,10 @@ void KeyVerificationSession::sendStartSas() void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) { + if (state() == ACCEPTED) { + // It's possible to receive ready and start in the same sync, in which case start might be handled before ready. + return; + } if (state() != WAITINGFORREADY) { cancelVerification(UNEXPECTED_MESSAGE); return; @@ -334,7 +338,7 @@ void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) { - if (state() != READY) { + if (state() != READY && state() != WAITINGFORREADY) { cancelVerification(UNEXPECTED_MESSAGE); return; } @@ -510,3 +514,8 @@ KeyVerificationSession::Error KeyVerificationSession::stringToError(const QStrin } return NONE; } + +QString KeyVerificationSession::remoteDeviceId() const +{ + return m_remoteDeviceId; +} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index aa0295cb..31f63453 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -19,6 +19,11 @@ struct QUOTIENT_API EmojiEntry { Q_GADGET Q_PROPERTY(QString emoji MEMBER emoji CONSTANT) Q_PROPERTY(QString description MEMBER description CONSTANT) + +public: + bool operator==(const EmojiEntry& rhs) const { + return emoji == rhs.emoji && description == rhs.description; + } }; /** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. @@ -93,6 +98,8 @@ public: Error error() const; + QString remoteDeviceId() const; + public Q_SLOTS: void sendRequest(); void sendReady(); -- cgit v1.2.3 From a3ab7dc4bc7cd538d922ea69d2dad158e7bc9770 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 7 Sep 2022 22:11:51 +0200 Subject: README: add a troubleshooting case with mixed Qt 5/6 --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 8be38b5c..6278535a 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,20 @@ by setting `Quotient_INSTALL_TESTS` to `OFF`. "Qt5Widgets", but CMake did not find one. ``` then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. + +- If `cmake` fails with a message similar to: + ``` + CMake Error at /usr/lib64/cmake/Qt6Core/Qt6CoreVersionlessTargets.cmake:37 (message): + Some (but not all) targets in this export set were already defined. + + Targets Defined: Qt::Core + + Targets not yet defined: Qt::CorePrivate + ``` + then you likely have both Qt 5 and Qt 6 on your system, and your client code + uses a different major version than Quotient. Make sure you use the client + version that matches libQuotient (e.g. you can't configure Quaternion 0.0.95 + with libQuotient 0.7 in Qt 6 mode). - If you use GCC and get an "unknown declarator" compilation error in the file `qtconcurrentthreadengine.h` - unfortunately, it is an actual error in Qt 5.15 -- cgit v1.2.3 From d863324d07ffafc2afcd65ab1655f1d1d01d1abc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 7 Sep 2022 09:25:27 +0200 Subject: makeImpl: add support for aggregate initialisation Since C++17, parentheses only work when a constructor is there, while braces allow both calling a constructor and aggregate initialisation. --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index 1cb1c0e1..9efda5d1 100644 --- a/lib/util.h +++ b/lib/util.h @@ -160,7 +160,7 @@ template makeImpl(ArgTs&&... args) { return ImplPtr { - new ImplType(std::forward(args)...), + new ImplType{std::forward(args)...}, [](TypeToDelete* impl) { delete impl; } }; } -- cgit v1.2.3 From 06d2ddafd856af68f39aa701570443a033e24643 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 10 Sep 2022 09:05:47 +0200 Subject: CMakeLists: adjust .cmake files install path on Windows With CMAKE_INSTALL_LIBDIR set to ".", the place .cmake files land in was not discoverable by find_package(), assuming a conventional CMAKE_PREFIX_PATH. --- CMakeLists.txt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c4d9545..f3149a19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,18 +50,22 @@ endif() if (WIN32) if (NOT CMAKE_INSTALL_LIBDIR) set(CMAKE_INSTALL_LIBDIR ".") - endif () + set(CMakeFilesLocation "cmake") + else() + set(CMakeFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") + endif() if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR ".") - endif () + endif() if (NOT CMAKE_INSTALL_INCLUDEDIR) set(CMAKE_INSTALL_INCLUDEDIR "include") - endif () + endif() else() include(GNUInstallDirs) set(INCLUDEDIR_INIT ${PROJECT_NAME}) + set(CMakeFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") endif(WIN32) set(${PROJECT_NAME}_INSTALL_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}/${INCLUDEDIR_INIT}" CACHE PATH @@ -357,14 +361,13 @@ configure_file(cmake/${PROJECT_NAME}Config.cmake.in @ONLY ) -set(ConfigFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") install(EXPORT ${PROJECT_NAME}Targets - FILE ${PROJECT_NAME}Targets.cmake DESTINATION ${ConfigFilesLocation}) + FILE ${PROJECT_NAME}Targets.cmake DESTINATION ${CMakeFilesLocation}) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}Config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}ConfigVersion.cmake" - DESTINATION ${ConfigFilesLocation} + DESTINATION ${CMakeFilesLocation} ) install(EXPORT_ANDROID_MK ${PROJECT_NAME}Targets DESTINATION ${CMAKE_INSTALL_DATADIR}/ndk-modules) -- cgit v1.2.3 From d890cc09dd6cd096423271ce47ca4ac86d2e9e41 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 10 Sep 2022 10:43:59 +0200 Subject: CMakeLists, MSVC: require new preprocessor in public The new MSVC preprocessor is needed to correctly deal with variadic macros defined in .h files, so the respective compiler switch has to be added when building depending projects too. --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f3149a19..0fbb4ef1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ endif(CMAKE_BUILD_TYPE) message(STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) include(CheckCXXCompilerFlag) if (MSVC) - add_compile_options(/Zc:preprocessor /EHsc /W4 + add_compile_options(/EHsc /W4 /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) @@ -298,6 +298,9 @@ set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY COMPATIBLE_INTERFACE_STRING ${PROJECT_NAME}_MAJOR_VERSION) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) +if (MSVC) + target_compile_options(${PROJECT_NAME} PUBLIC /Zc:preprocessor) +endif() # Don't use PCH w/GCC (https://bugzilla.redhat.com/show_bug.cgi?id=1721553#c34) if (NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) -- cgit v1.2.3 From 83746abdf677f573287be2c93d2ad12b1aa84b3f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 10 Sep 2022 13:42:39 +0200 Subject: Update lib/keyverificationsession.h Co-authored-by: Alexey Rusakov --- lib/keyverificationsession.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index 31f63453..b2e3b36d 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -21,9 +21,7 @@ struct QUOTIENT_API EmojiEntry { Q_PROPERTY(QString description MEMBER description CONSTANT) public: - bool operator==(const EmojiEntry& rhs) const { - return emoji == rhs.emoji && description == rhs.description; - } + bool operator==(const EmojiEntry& rhs) const = default; }; /** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. -- cgit v1.2.3 From fa34778cc377ab98a0b8f3944063e426c73a3941 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 10 Sep 2022 13:50:13 +0200 Subject: Remove header for keyverificationtest --- autotests/testkeyverification.cpp | 83 ++++++++++++++++++++++----------------- autotests/testkeyverification.h | 18 --------- 2 files changed, 46 insertions(+), 55 deletions(-) delete mode 100644 autotests/testkeyverification.h diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp index fbbb7614..59a1c934 100644 --- a/autotests/testkeyverification.cpp +++ b/autotests/testkeyverification.cpp @@ -2,49 +2,58 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "testkeyverification.h" -#include "qt_connection_util.h" -void TestKeyVerificationSession::testVerification() +#include +#include "testutils.h" +#include + +class TestKeyVerificationSession : public QObject { - CREATE_CONNECTION(a, "alice1", "secret", "AliceDesktop") - CREATE_CONNECTION(b, "alice1", "secret", "AlicePhone") + Q_OBJECT + +private Q_SLOTS: + void testVerification() + { + CREATE_CONNECTION(a, "alice1", "secret", "AliceDesktop") + CREATE_CONNECTION(b, "alice1", "secret", "AlicePhone") - KeyVerificationSession *aSession = nullptr; - connect(a.get(), &Connection::newKeyVerificationSession, this, [&](KeyVerificationSession* session) { - aSession = session; - QVERIFY(session->remoteDeviceId() == b->deviceId()); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY); - connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ - QVERIFY(session->state() == KeyVerificationSession::ACCEPTED); + KeyVerificationSession *aSession = nullptr; + connect(a.get(), &Connection::newKeyVerificationSession, this, [&](KeyVerificationSession* session) { + aSession = session; + QVERIFY(session->remoteDeviceId() == b->deviceId()); + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY); connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); - session->sendMac(); + QVERIFY(session->state() == KeyVerificationSession::ACCEPTED); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); + session->sendMac(); + }); }); }); - }); - a->startKeyVerificationSession(b->deviceId()); - connect(b.get(), &Connection::newKeyVerificationSession, this, [=](KeyVerificationSession* session) { - QVERIFY(session->remoteDeviceId() == a->deviceId()); - QVERIFY(session->state() == KeyVerificationSession::INCOMING); - session->sendReady(); - // KeyVerificationSession::READY is skipped because we have only one method - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT); - connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORKEY || session->state() == KeyVerificationSession::ACCEPTED); - connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=]() { - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); - QVERIFY(aSession); - QVERIFY(aSession->sasEmojis() == session->sasEmojis()); - session->sendMac(); - QVERIFY(session->state() == KeyVerificationSession::WAITINGFORMAC); + a->startKeyVerificationSession(b->deviceId()); + connect(b.get(), &Connection::newKeyVerificationSession, this, [=](KeyVerificationSession* session) { + QVERIFY(session->remoteDeviceId() == a->deviceId()); + QVERIFY(session->state() == KeyVerificationSession::INCOMING); + session->sendReady(); + // KeyVerificationSession::READY is skipped because we have only one method + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORKEY || session->state() == KeyVerificationSession::ACCEPTED); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=]() { + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); + QVERIFY(aSession); + QVERIFY(aSession->sasEmojis() == session->sasEmojis()); + session->sendMac(); + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORMAC); + }); }); - }); - }); - b->syncLoop(); - a->syncLoop(); - QSignalSpy spy(b.get(), &Connection::incomingKeyVerificationDone); - spy.wait(10000); -} + }); + b->syncLoop(); + a->syncLoop(); + QSignalSpy spy(b.get(), &Connection::incomingKeyVerificationDone); + spy.wait(10000); + } +}; QTEST_GUILESS_MAIN(TestKeyVerificationSession) +#include "testkeyverification.moc" diff --git a/autotests/testkeyverification.h b/autotests/testkeyverification.h deleted file mode 100644 index 28e00e11..00000000 --- a/autotests/testkeyverification.h +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Tobias Fella -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include "testutils.h" - -class TestKeyVerificationSession : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void testVerification(); - -}; - -- cgit v1.2.3 From 20f01303b40df8b7710dfa560940a7c71c1e9c19 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 10 Sep 2022 10:43:59 +0200 Subject: Adjust converters.h logic to support Qt 6.4 There's a bit convoluted stack of calls involved here, worth laying out. C++ containers are loaded from JSON containers by calling fromJson<> on each element of the JSON container, specialised by the element type of the C++ container. If that element type is not itself an object (e.g., QString), the respective specialisation of fromJson<> is supposed to kick-in and override the default template that delegates the conversion to JsonConverter (which in turn falls back to JsonObjectConverter unless specialised for that type). Because template functions cannot be partially specialised, that specialised overload for, say, QString, is not complete: it accepts QJsonValue but anything except QJsonValue will hit the generic overload instead. That makes the whole fromJson<> machinery quite sensitive to the exact JSON type passed to it; but as of Qt 5, the types actually presented to fromJson() were limited to QJsonValue and QJsonDocument (okay, QJsonObject and QJsonArray could also be there but the QJsonObject case is trivial for JsonConverter and containers loaded from QJsonArray are all caught with JsonArrayConverter). Qt 6 started returning (const) QJsonValueRef from `QJson*::const_iterator::operator*()` - meaning that whenever a simple type (like, again, QString) is loaded within the bigger container, the "wrong" fromJson() gets called. To fix this, JsonObjectUnpacker gained a dedicated function to unpack QJsonValueRef. QJsonValueRef is an old name existing since Qt 5.15 - no big pain. However, in Qt 6.4 QJsonValueConstRef is getting introduced for the same purpose. It's possible to wrap the *Ref overloads in some #if/#else brackets but at that point it becomes easier to just produce QJsonValue from whatever QJson* iterators dereference to. --- lib/converters.h | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index 688f7bbd..0fb36320 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -37,7 +37,6 @@ struct JsonObjectUnpacker { // fromJson specialisation instead of specialising // the entire JsonConverter; if a different type of JSON value is needed // (e.g., an array), specialising JsonConverter is inevitable - static T load(QJsonValueRef jvr) { return fromJson(QJsonValue(jvr)); } static T load(const QJsonValue& jv) { return fromJson(jv.toObject()); } static T load(const QJsonDocument& jd) { return fromJson(jd.object()); } }; @@ -228,7 +227,7 @@ QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues) return {}; } -// JsonConverter<> specialisations +// Specialisations template<> inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); } @@ -248,7 +247,7 @@ inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } -//! Use fromJson and use toLatin1()/toUtf8()/... to make QByteArray +//! Use fromJson and then toLatin1()/toUtf8()/... to make QByteArray //! //! QJsonValue can only convert to QString and there's ambiguity whether //! conversion to QByteArray should use (fast but very limited) toLatin1() or @@ -348,8 +347,12 @@ struct JsonArrayConverter { { VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); - for (const auto& i : ja) - vect.push_back(fromJson(i)); + // NB: Make sure to pass QJsonValue to fromJson<> so that it could + // hit the correct overload and not fall back to the generic fromJson + // that treats everything as an object. See also the explanation in + // the commit introducing these lines. + for (const QJsonValue v : ja) + vect.push_back(fromJson(v)); return vect; } static auto load(const QJsonValue& jv) { return load(jv.toArray()); } @@ -401,8 +404,11 @@ struct HashMapFromJson { static void fillFrom(const QJsonObject& jo, HashMapT& h) { h.reserve(h.size() + jo.size()); + // NB: the QJsonValue cast below is for the same reason as in + // JsonArrayConverter for (auto it = jo.begin(); it != jo.end(); ++it) - h[it.key()] = fromJson(it.value()); + h[it.key()] = fromJson( + QJsonValue(it.value())); } }; -- cgit v1.2.3 From 6abdd5358c9c5ed89cda5bc5e50a76af423b0634 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 11 Sep 2022 20:25:46 +0200 Subject: KeyVerificationEvent; KeyVerificationSession::handleEvent() Key verification events gain their own base type and KeyVerificationSession gets a single point of entry for all kinds of incoming events. This allows to drop a pile of `incoming*` signals in Connection and a stack of options inside switchOnType in processIfVerification(). KVS::handleEvent() also makes (some) allowed state transitions a bit clearer. --- autotests/testkeyverification.cpp | 4 +- lib/connection.cpp | 44 +++++-------- lib/connection.h | 11 +--- lib/events/keyverificationevent.h | 74 ++++++++------------- lib/keyverificationsession.cpp | 133 ++++++++++++++------------------------ lib/keyverificationsession.h | 6 +- 6 files changed, 102 insertions(+), 170 deletions(-) diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp index 59a1c934..96aad6c7 100644 --- a/autotests/testkeyverification.cpp +++ b/autotests/testkeyverification.cpp @@ -17,7 +17,7 @@ private Q_SLOTS: CREATE_CONNECTION(a, "alice1", "secret", "AliceDesktop") CREATE_CONNECTION(b, "alice1", "secret", "AlicePhone") - KeyVerificationSession *aSession = nullptr; + QPointer aSession{}; connect(a.get(), &Connection::newKeyVerificationSession, this, [&](KeyVerificationSession* session) { aSession = session; QVERIFY(session->remoteDeviceId() == b->deviceId()); @@ -51,7 +51,7 @@ private Q_SLOTS: }); b->syncLoop(); a->syncLoop(); - QSignalSpy spy(b.get(), &Connection::incomingKeyVerificationDone); + QSignalSpy spy(aSession, &KeyVerificationSession::finished); spy.wait(10000); } }; diff --git a/lib/connection.cpp b/lib/connection.cpp index 53c99969..5003f40c 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -124,6 +124,7 @@ public: // A map from SenderKey to vector of InboundSession UnorderedMap> olmSessions; + QHash verificationSessions; #endif GetCapabilitiesJob* capabilitiesJob = nullptr; @@ -971,6 +972,8 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; for (auto&& tdEvt : toDeviceEvents) { + if (processIfVerificationEvent(*tdEvt, false)) + continue; if (auto&& event = eventCast(std::move(tdEvt))) { if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { qCDebug(E2EE) << "Unsupported algorithm" << event->id() @@ -985,9 +988,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) outdatedUsers += event->senderId(); encryptionUpdateRequired = true; pendingEncryptedEvents.push_back(std::move(event)); - continue; } - processIfVerificationEvent(*tdEvt, false); } } #endif @@ -998,33 +999,22 @@ bool Connection::Private::processIfVerificationEvent(const Event& evt, bool encrypted) { return switchOnType(evt, - [this, encrypted](const KeyVerificationRequestEvent& event) { - auto session = - new KeyVerificationSession(q->userId(), event, q, encrypted); - emit q->newKeyVerificationSession(session); - return true; - }, [this](const KeyVerificationReadyEvent& event) { - emit q->incomingKeyVerificationReady(event); - return true; - }, [this](const KeyVerificationStartEvent& event) { - emit q->incomingKeyVerificationStart(event); - return true; - }, [this](const KeyVerificationAcceptEvent& event) { - emit q->incomingKeyVerificationAccept(event); - return true; - }, [this](const KeyVerificationKeyEvent& event) { - emit q->incomingKeyVerificationKey(event); + [this, encrypted](const KeyVerificationRequestEvent& reqEvt) { + const auto sessionIter = verificationSessions.insert( + reqEvt.transactionId(), + new KeyVerificationSession(q->userId(), reqEvt, q, encrypted)); + emit q->newKeyVerificationSession(*sessionIter); return true; - }, [this](const KeyVerificationMacEvent& event) { - emit q->incomingKeyVerificationMac(event); - return true; - }, [this](const KeyVerificationDoneEvent& event) { - emit q->incomingKeyVerificationDone(event); - return true; - }, [this](const KeyVerificationCancelEvent& event) { - emit q->incomingKeyVerificationCancel(event); + }, + [this](const KeyVerificationEvent& kvEvt) { + if (auto* const session = + verificationSessions.value(kvEvt.transactionId())) { + session->handleEvent(kvEvt); + emit q->keyVerificationStateChanged(session, session->state()); + } return true; - }, false); + }, + false); } void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) diff --git a/lib/connection.h b/lib/connection.h index 79c23c9a..75faf370 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -864,15 +864,10 @@ Q_SIGNALS: void devicesListLoaded(); #ifdef Quotient_E2EE_ENABLED - void incomingKeyVerificationReady(const KeyVerificationReadyEvent& event); - void incomingKeyVerificationStart(const KeyVerificationStartEvent& event); - void incomingKeyVerificationAccept(const KeyVerificationAcceptEvent& event); - void incomingKeyVerificationKey(const KeyVerificationKeyEvent& event); - void incomingKeyVerificationMac(const KeyVerificationMacEvent& event); - void incomingKeyVerificationDone(const KeyVerificationDoneEvent& event); - void incomingKeyVerificationCancel(const KeyVerificationCancelEvent& event); - void newKeyVerificationSession(KeyVerificationSession* session); + void keyVerificationStateChanged( + const KeyVerificationSession* session, + Quotient::KeyVerificationSession::State state); void sessionVerified(const QString& userId, const QString& deviceId); #endif diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 0ffd8b2c..0e939508 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -9,13 +9,24 @@ namespace Quotient { static constexpr auto SasV1Method = "m.sas.v1"_ls; +class QUOTIENT_API KeyVerificationEvent : public Event { +public: + QUO_BASE_EVENT(KeyVerificationEvent, "m.key.*"_ls, Event::BaseMetaType) + + using Event::Event; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QUO_CONTENT_GETTER(QString, transactionId) +}; + /// Requests a key verification with another user's devices. /// Typically sent as a to-device event. -class QUOTIENT_API KeyVerificationRequestEvent : public Event { +class QUOTIENT_API KeyVerificationRequestEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationRequestEvent, "m.key.verification.request") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationRequestEvent(const QString& transactionId, const QString& fromDevice, const QStringList& methods, @@ -30,10 +41,6 @@ public: /// The device ID which is initiating the request. QUO_CONTENT_GETTER(QString, fromDevice) - /// An opaque identifier for the verification request. Must - /// be unique with respect to the devices involved. - QUO_CONTENT_GETTER(QString, transactionId) - /// The verification methods supported by the sender. QUO_CONTENT_GETTER(QStringList, methods) @@ -44,11 +51,11 @@ public: QUO_CONTENT_GETTER(QDateTime, timestamp) }; -class QUOTIENT_API KeyVerificationReadyEvent : public Event { +class QUOTIENT_API KeyVerificationReadyEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationReadyEvent, "m.key.verification.ready") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationReadyEvent(const QString& transactionId, const QString& fromDevice, const QStringList& methods) @@ -61,19 +68,16 @@ public: /// The device ID which is accepting the request. QUO_CONTENT_GETTER(QString, fromDevice) - /// The transaction id of the verification request - QUO_CONTENT_GETTER(QString, transactionId) - /// The verification methods supported by the sender. QUO_CONTENT_GETTER(QStringList, methods) }; /// Begins a key verification process. -class QUOTIENT_API KeyVerificationStartEvent : public Event { +class QUOTIENT_API KeyVerificationStartEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationStartEvent, "m.key.verification.start") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationStartEvent(const QString& transactionId, const QString& fromDevice) : KeyVerificationStartEvent( @@ -92,10 +96,6 @@ public: /// The device ID which is initiating the process. QUO_CONTENT_GETTER(QString, fromDevice) - /// An opaque identifier for the verification request. Must - /// be unique with respect to the devices involved. - QUO_CONTENT_GETTER(QString, transactionId) - /// The verification method to use. QUO_CONTENT_GETTER(QString, method) @@ -140,11 +140,11 @@ public: /// Accepts a previously sent m.key.verification.start message. /// Typically sent as a to-device event. -class QUOTIENT_API KeyVerificationAcceptEvent : public Event { +class QUOTIENT_API KeyVerificationAcceptEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationAcceptEvent, "m.key.verification.accept") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationAcceptEvent(const QString& transactionId, const QString& commitment) : KeyVerificationAcceptEvent(basicJson( @@ -158,9 +158,6 @@ public: { "commitment"_ls, commitment } })) {} - /// An opaque identifier for the verification process. - QUO_CONTENT_GETTER(QString, transactionId) - /// The verification method to use. Must be 'm.sas.v1'. QUO_CONTENT_GETTER(QString, method) @@ -170,10 +167,7 @@ public: /// The hash method the device is choosing to use, out of the /// options in the m.key.verification.start message. - QString hashData() const - { - return contentPart("hash"_ls); - } + QUO_CONTENT_GETTER_X(QString, hashData, "hash"_ls) /// The message authentication code the device is choosing to use, out /// of the options in the m.key.verification.start message. @@ -188,11 +182,11 @@ public: QUO_CONTENT_GETTER(QString, commitment) }; -class QUOTIENT_API KeyVerificationCancelEvent : public Event { +class QUOTIENT_API KeyVerificationCancelEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationCancelEvent, "m.key.verification.cancel") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationCancelEvent(const QString& transactionId, const QString& reason) : KeyVerificationCancelEvent( @@ -203,9 +197,6 @@ public: })) {} - /// An opaque identifier for the verification process. - QUO_CONTENT_GETTER(QString, transactionId) - /// A human readable description of the code. The client should only /// rely on this string if it does not understand the code. QUO_CONTENT_GETTER(QString, reason) @@ -216,30 +207,27 @@ public: /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. -class QUOTIENT_API KeyVerificationKeyEvent : public Event { +class KeyVerificationKeyEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationKeyEvent(const QString& transactionId, const QString& key) : KeyVerificationKeyEvent( basicJson(TypeId, { { "transaction_id"_ls, transactionId }, { "key"_ls, key } })) {} - /// An opaque identifier for the verification process. - QUO_CONTENT_GETTER(QString, transactionId) - /// The device's ephemeral public key, encoded as unpadded base64. QUO_CONTENT_GETTER(QString, key) }; /// Sends the MAC of a device's key to the partner device. -class QUOTIENT_API KeyVerificationMacEvent : public Event { +class QUOTIENT_API KeyVerificationMacEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationMacEvent, "m.key.verification.mac") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; KeyVerificationMacEvent(const QString& transactionId, const QString& keys, const QJsonObject& mac) : KeyVerificationMacEvent( @@ -248,9 +236,6 @@ public: { "mac"_ls, mac } })) {} - /// An opaque identifier for the verification process. - QUO_CONTENT_GETTER(QString, transactionId) - /// The device's ephemeral public key, encoded as unpadded base64. QUO_CONTENT_GETTER(QString, keys) @@ -260,17 +245,14 @@ public: } }; -class QUOTIENT_API KeyVerificationDoneEvent : public Event { +class QUOTIENT_API KeyVerificationDoneEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationDoneEvent, "m.key.verification.done") - using Event::Event; + using KeyVerificationEvent::KeyVerificationEvent; explicit KeyVerificationDoneEvent(const QString& transactionId) : KeyVerificationDoneEvent( basicJson(TypeId, { { "transaction_id"_ls, transactionId } })) {} - - /// The same transactionId as before - QUO_CONTENT_GETTER(QString, transactionId) }; } // namespace Quotient diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 24fc08e1..cc4428d7 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -68,42 +68,6 @@ KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, void KeyVerificationSession::init(milliseconds timeout) { - connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { - if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { - handleReady(event); - } - }); - connect(m_connection, &Connection::incomingKeyVerificationStart, this, [this](const KeyVerificationStartEvent& event) { - if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { - handleStart(event); - } - }); - connect(m_connection, &Connection::incomingKeyVerificationAccept, this, [this](const KeyVerificationAcceptEvent& event) { - if (event.transactionId() == m_transactionId) { - handleAccept(event); - } - }); - connect(m_connection, &Connection::incomingKeyVerificationKey, this, [this](const KeyVerificationKeyEvent& event) { - if (event.transactionId() == m_transactionId) { - handleKey(event); - } - }); - connect(m_connection, &Connection::incomingKeyVerificationMac, this, [this](const KeyVerificationMacEvent& event) { - if (event.transactionId() == m_transactionId) { - handleMac(event); - } - }); - connect(m_connection, &Connection::incomingKeyVerificationDone, this, [this](const KeyVerificationDoneEvent& event) { - if (event.transactionId() == m_transactionId) { - handleDone(event); - } - }); - connect(m_connection, &Connection::incomingKeyVerificationCancel, this, [this](const KeyVerificationCancelEvent& event) { - if (event.transactionId() == m_transactionId) { - handleCancel(event); - } - }); - QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); m_sas = olm_sas(new std::byte[olm_sas_size()]); @@ -118,6 +82,53 @@ KeyVerificationSession::~KeyVerificationSession() delete[] reinterpret_cast(m_sas); } +void KeyVerificationSession::handleEvent(const KeyVerificationEvent& baseEvent) +{ + if (!switchOnType( + baseEvent, + [this](const KeyVerificationCancelEvent& event) { + setError(stringToError(event.code())); + setState(CANCELED); + return true; + }, + [this](const KeyVerificationStartEvent& event) { + if (state() != WAITINGFORREADY && state() != READY) + return false; + handleStart(event); + return true; + }, + [this](const KeyVerificationReadyEvent& event) { + if (state() == WAITINGFORREADY) + handleReady(event); + // ACCEPTED is also fine here because it's possible to receive + // ready and start in the same sync, in which case start might + // be handled before ready. + return state() == WAITINGFORREADY || state() == ACCEPTED; + }, + [this](const KeyVerificationAcceptEvent& event) { + if (state() != WAITINGFORACCEPT) + return false; + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); + return true; + }, + [this](const KeyVerificationKeyEvent& event) { + if (state() != ACCEPTED && state() != WAITINGFORKEY) + return false; + handleKey(event); + return true; + }, + [this](const KeyVerificationMacEvent& event) { + if (state() != WAITINGFORMAC) + return false; + handleMac(event); + return true; + }, + [this](const KeyVerificationDoneEvent&) { return state() == DONE; })) + cancelVerification(UNEXPECTED_MESSAGE); +} + struct EmojiStoreEntry : EmojiEntry { QHash translatedDescriptions; @@ -154,10 +165,6 @@ EmojiEntry emojiForCode(int code, const QString& language) void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) { - if (state() != WAITINGFORKEY && state() != ACCEPTED) { - cancelVerification(UNEXPECTED_MESSAGE); - return; - } auto eventKey = event.key().toLatin1(); olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); @@ -314,34 +321,18 @@ void KeyVerificationSession::sendStartSas() void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) { - if (state() == ACCEPTED) { - // It's possible to receive ready and start in the same sync, in which case start might be handled before ready. - return; - } - if (state() != WAITINGFORREADY) { - cancelVerification(UNEXPECTED_MESSAGE); - return; - } setState(READY); m_remoteSupportedMethods = event.methods(); auto methods = commonSupportedMethods(m_remoteSupportedMethods); - if (methods.isEmpty()) { + if (methods.isEmpty()) cancelVerification(UNKNOWN_METHOD); - return; - } - - if (methods.size() == 1) { - sendStartSas(); - } + else if (methods.size() == 1) + sendStartSas(); // -> WAITINGFORACCEPT } void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) { - if (state() != READY && state() != WAITINGFORREADY) { - cancelVerification(UNEXPECTED_MESSAGE); - return; - } if (startSentByUs) { if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { return; @@ -362,17 +353,6 @@ void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) setState(ACCEPTED); } -void KeyVerificationSession::handleAccept(const KeyVerificationAcceptEvent& event) -{ - if(state() != WAITINGFORACCEPT) { - cancelVerification(UNEXPECTED_MESSAGE); - return; - } - m_commitment = event.commitment(); - sendKey(); - setState(WAITINGFORKEY); -} - void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) { QStringList keys = event.mac().keys(); @@ -402,19 +382,6 @@ void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) } } -void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent&) -{ - if (state() != DONE) { - cancelVerification(UNEXPECTED_MESSAGE); - } -} - -void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& event) -{ - setError(stringToError(event.code())); - setState(CANCELED); -} - QVector KeyVerificationSession::sasEmojis() const { return m_sasEmojis; diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index b2e3b36d..9cac1184 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -91,6 +91,8 @@ public: ~KeyVerificationSession() override; Q_DISABLE_COPY_MOVE(KeyVerificationSession) + void handleEvent(const KeyVerificationEvent& baseEvent); + QVector sasEmojis() const; State state() const; @@ -108,7 +110,6 @@ public Q_SLOTS: void cancelVerification(Error error); Q_SIGNALS: - void startReceived(); void keyReceived(); void sasEmojisChanged(); void stateChanged(); @@ -133,11 +134,8 @@ private: void handleReady(const KeyVerificationReadyEvent& event); void handleStart(const KeyVerificationStartEvent& event); - void handleAccept(const KeyVerificationAcceptEvent& event); void handleKey(const KeyVerificationKeyEvent& event); void handleMac(const KeyVerificationMacEvent& event); - void handleDone(const KeyVerificationDoneEvent&); - void handleCancel(const KeyVerificationCancelEvent& event); void init(std::chrono::milliseconds timeout); void setState(State state); void setError(Error error); -- cgit v1.2.3 From 79f995da07ca240c768281bf1d040eb94c07583e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 11 Sep 2022 21:46:21 +0200 Subject: TestKeyVerification: Add a missing allowed state --- autotests/testkeyverification.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp index 96aad6c7..1fa6d8c6 100644 --- a/autotests/testkeyverification.cpp +++ b/autotests/testkeyverification.cpp @@ -23,7 +23,7 @@ private Q_SLOTS: QVERIFY(session->remoteDeviceId() == b->deviceId()); QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY); connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ - QVERIFY(session->state() == KeyVerificationSession::ACCEPTED); + QVERIFY(session->state() == KeyVerificationSession::ACCEPTED || session->state() == KeyVerificationSession::READY); connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); session->sendMac(); -- cgit v1.2.3 From d7030d88eee9a1ac285f00f3a252b63b9cd40a6c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 12 Sep 2022 08:27:38 +0200 Subject: Fix a leak in Connection::saveAccessTokenToKeychain() --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 53c99969..5de02b8c 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -379,7 +379,7 @@ public: { qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); auto job = new QKeychain::WritePasswordJob(qAppName()); - job->setAutoDelete(false); + job->setAutoDelete(true); job->setKey(q->userId()); job->setBinaryData(data->accessToken()); job->start(); -- cgit v1.2.3 From f4009215b500dbae4a10d1a86bb059272e47d0cb Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 12 Sep 2022 18:56:54 +0200 Subject: Only trust verification keys if the user verified the SAS --- lib/keyverificationsession.cpp | 15 ++++++++++++++- lib/keyverificationsession.h | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index c6b62a83..541ca49b 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -249,6 +249,10 @@ void KeyVerificationSession::sendMac() mac), m_encrypted); setState (macReceived ? DONE : WAITINGFORMAC); + m_verified = true; + if (!m_pendingEdKeyId.isEmpty()) { + trustKeys(); + } } void KeyVerificationSession::sendDone() @@ -387,7 +391,16 @@ void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) return; } - m_connection->database()->setSessionVerified(edKeyId); + m_pendingEdKeyId = edKeyId; + + if (m_verified) { + trustKeys(); + } +} + +void KeyVerificationSession::trustKeys() +{ + m_connection->database()->setSessionVerified(m_pendingEdKeyId); emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); macReceived = true; diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h index aa0295cb..075ea1e2 100644 --- a/lib/keyverificationsession.h +++ b/lib/keyverificationsession.h @@ -125,6 +125,8 @@ private: bool macReceived = false; bool m_encrypted; QStringList m_remoteSupportedMethods; + bool m_verified = false; + QString m_pendingEdKeyId{}; void handleReady(const KeyVerificationReadyEvent& event); void handleStart(const KeyVerificationStartEvent& event); @@ -138,6 +140,7 @@ private: void setError(Error error); static QString errorToString(Error error); static Error stringToError(const QString& error); + void trustKeys(); QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); -- cgit v1.2.3 From 0d0a91b4438d68c833f480f45017847829bf6088 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 12 Sep 2022 21:08:03 +0200 Subject: NetworkAccessManager: QThreadStorage -> thread_local It's more straightforward in usage and less prone to leaks (QThreadStorage doesn't delete objects when on non-main thread). --- lib/networkaccessmanager.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 38ab07cc..78be89bd 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -9,7 +9,6 @@ #include "mxcreply.h" #include -#include #include #include @@ -50,9 +49,8 @@ QList NetworkAccessManager::ignoredSslErrors() const void NetworkAccessManager::ignoreSslErrors(bool ignore) const { if (ignore) { - connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList &errors) { - reply->ignoreSslErrors(); - }); + connect(this, &QNetworkAccessManager::sslErrors, this, + [](QNetworkReply* reply) { reply->ignoreSslErrors(); }); } else { disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr); } @@ -70,11 +68,8 @@ void NetworkAccessManager::clearIgnoredSslErrors() NetworkAccessManager* NetworkAccessManager::instance() { - static QThreadStorage storage; - if(!storage.hasLocalData()) { - storage.setLocalData(new NetworkAccessManager()); - } - return storage.localData(); + thread_local NetworkAccessManager nam; + return &nam; } QNetworkReply* NetworkAccessManager::createRequest( -- cgit v1.2.3 From 51ebb9166df1dee7831d9ab25aaa8e70bd65aa16 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 14 Sep 2022 20:59:11 +0200 Subject: CMakeLists: add flags more carefully for non-Windows Previous logic wasn't quite accurate with detection of existing flags in CMAKE_CXX_FLAGS - the new code doesn't add a flag if the negating flag is already there. This required separate treatment of positive and negative (-Wno-*) flags. --- CMakeLists.txt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fbb4ef1..4781ed52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,14 +37,20 @@ if (MSVC) /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() - foreach (FLAG Wall Wpedantic Wextra Werror=return-type Wno-unused-parameter - Wno-gnu-zero-variadic-macro-arguments Wno-subobject-linkage) - CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) - if ( COMPILER_${FLAG}_SUPPORTED AND - NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") - add_compile_options(-${FLAG}) + foreach (FLAG all pedantic extra error=return-type) # Switch these on + CHECK_CXX_COMPILER_FLAG("-W${FLAG}" W${FLAG}_SUPPORTED) + if (W${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") + add_compile_options(-W${FLAG}) endif () endforeach () + foreach (FLAG unused-parameter gnu-zero-variadic-macro-arguments + subobject-linkage) # Switch these off + CHECK_CXX_COMPILER_FLAG("-Wno-${FLAG}" Wno-${FLAG}_SUPPORTED) + if (Wno-${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") + add_compile_options(-Wno-${FLAG}) + endforeach () endif() if (WIN32) -- cgit v1.2.3 From 7fea15b9942b69a03796b6b2b104a208c82c1806 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 14 Sep 2022 21:12:18 +0200 Subject: Fix a CMake-breaking typo in the just commited code --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4781ed52..91ee8217 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ else() if (Wno-${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") add_compile_options(-Wno-${FLAG}) + endif() endforeach () endif() -- 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 ++++++---- lib/room.h | 54 +++++++++++++++++++++++++++--------------------------- 2 files changed, 33 insertions(+), 31 deletions(-) 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(); } diff --git a/lib/room.h b/lib/room.h index 5eb66b8b..e2d6b869 100644 --- a/lib/room.h +++ b/lib/room.h @@ -187,28 +187,28 @@ public: //! each change. Specific enumerators mention these individual signals. //! \sa changed enum class Change : uint { - None = 0x0, //< No changes occurred in the room - Name = 0x1, //< \sa namesChanged, displaynameChanged - Aliases = 0x2, //< \sa namesChanged, displaynameChanged + None = 0x0, ///< No changes occurred in the room + Name = 0x1, ///< \sa namesChanged, displaynameChanged + Aliases = 0x2, ///< \sa namesChanged, displaynameChanged CanonicalAlias = Aliases, - Topic = 0x4, //< \sa topicChanged - PartiallyReadStats = 0x8, //< \sa partiallyReadStatsChanged + Topic = 0x4, ///< \sa topicChanged + PartiallyReadStats = 0x8, ///< \sa partiallyReadStatsChanged DECL_DEPRECATED_ENUMERATOR(UnreadNotifs, PartiallyReadStats), - Avatar = 0x10, //< \sa avatarChanged - JoinState = 0x20, //< \sa joinStateChanged - Tags = 0x40, //< \sa tagsChanged + Avatar = 0x10, ///< \sa avatarChanged + JoinState = 0x20, ///< \sa joinStateChanged + Tags = 0x40, ///< \sa tagsChanged //! \sa userAdded, userRemoved, memberRenamed, memberListChanged, //! displaynameChanged Members = 0x80, - UnreadStats = 0x100, //< \sa unreadStatsChanged + UnreadStats = 0x100, ///< \sa unreadStatsChanged AccountData Q_DECL_ENUMERATOR_DEPRECATED_X( "Change::AccountData will be merged into Change::Other in 0.8") = 0x200, - Summary = 0x400, //< \sa summaryChanged, displaynameChanged + Summary = 0x400, ///< \sa summaryChanged, displaynameChanged ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X( "Change::ReadMarker will be merged into Change::Other in 0.8") = 0x800, - Highlights = 0x1000, //< \sa highlightCountChanged + Highlights = 0x1000, ///< \sa highlightCountChanged //! A catch-all value that covers changes not listed above (such as //! encryption turned on or the room having been upgraded), as well as //! changes in the room state that the library is not aware of (e.g., @@ -626,7 +626,7 @@ public: //! \sa unreadStats, lastLocalReadReceipt qsizetype notificationCount() const; - //! \deprecated Use setReadReceipt() to drive changes in notification count + [[deprecated("Use setReadReceipt() to drive changes in notification count")]] Q_INVOKABLE void resetNotificationCount(); //! \brief Get the number of highlights since the last read receipt @@ -637,7 +637,7 @@ public: //! \sa unreadStats, lastLocalReadReceipt qsizetype highlightCount() const; - //! \deprecated Use setReadReceipt() to drive changes in highlightCount + [[deprecated("Use setReadReceipt() to drive changes in highlightCount")]] Q_INVOKABLE void resetHighlightCount(); /** Check whether the room has account data of the given type @@ -673,12 +673,12 @@ public: * actions on the room to its predecessors and successors. */ enum ActionScope { - ThisRoomOnly, //< Do not apply to predecessors and successors - WithinSameState, //< Apply to predecessors and successors in the same - //< state as the current one - OmitLeftState, //< Apply to all reachable predecessors and successors - //< except those in Leave state - WholeSequence //< Apply to all reachable predecessors and successors + ThisRoomOnly, ///< Do not apply to predecessors and successors + WithinSameState, ///< Apply to predecessors and successors in the same + ///< state as the current one + OmitLeftState, ///< Apply to all reachable predecessors and successors + ///< except those in Leave state + WholeSequence ///< Apply to all reachable predecessors and successors }; /** Overwrite the room's tags @@ -874,8 +874,8 @@ public Q_SLOTS: void inviteCall(const QString& callId, const int lifetime, const QString& sdp); void sendCallCandidates(const QString& callId, const QJsonArray& candidates); - //! \deprecated Lifetime argument is no more passed; use 2-arg - //! Room::answerCall() instead + [[deprecated("Lifetime argument is no more passed; " + "use 2-arg Room::answerCall() instead")]] void answerCall(const QString& callId, int lifetime, const QString& sdp); void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); @@ -970,8 +970,8 @@ Q_SIGNALS: Quotient::JoinState newState); void typingChanged(); - void highlightCountChanged(); //< \sa highlightCount - void notificationCountChanged(); //< \sa notificationCount + void highlightCountChanged(); ///< \sa highlightCount + void notificationCountChanged(); ///< \sa notificationCount void displayedChanged(bool displayed); void firstDisplayedEventChanged(); @@ -980,13 +980,13 @@ Q_SIGNALS: //! \sa lastReadReceipt void lastReadEventChanged(QVector userIds); void fullyReadMarkerMoved(QString fromEventId, QString toEventId); - //! \deprecated since 0.7 - use fullyReadMarkerMoved + [[deprecated("Since 0.7, use fullyReadMarkerMoved")]] void readMarkerMoved(QString fromEventId, QString toEventId); - //! \deprecated since 0.7 - use lastReadEventChanged + [[deprecated("Since 0.7, use lastReadEventChanged")]] void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, QString toEventId); - //! \deprecated since 0.7 - use either partiallyReadStatsChanged - //! or unreadStatsChanged + [[deprecated("Since 0.7, use either partiallyReadStatsChanged " + "or unreadStatsChanged")]] void unreadMessagesChanged(Quotient::Room* room); void partiallyReadStatsChanged(); void unreadStatsChanged(); -- 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(-) 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 747913f2549c1b228395192a73231b96c93f4f40 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 17 Sep 2022 22:11:53 +0200 Subject: Update README on E2EE [skip ci] --- README.md | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6278535a..e7597fce 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ in SECURITY.md. ## Getting and using libQuotient Depending on your platform, the library can be obtained from a package -management system. Recent releases of Debian and openSUSE, e.g., already have -it. Alternatively, just build the library from the source and bundle it with +management system. Recent releases of Fedora, Debian and openSUSE already have +it. Alternatively, you can build the library from the source and bundle it with your application, as described below. ### Pre-requisites @@ -39,12 +39,16 @@ your application, as described below. mobile operating systems where Qt is available might work too) - Recent enough Linux examples: Debian Bullseye; Fedora 35; openSUSE Leap 15.4; Ubuntu 22.04 LTS. -- Qt 5 (either Open Source or Commercial), 5.15 or higher -- CMake 3.16 or newer (from your package management system or - [the official website](https://cmake.org/download/)) -- A C++ toolchain with that supports at least some subset of C++20: +- Qt 5.15 or 6 (experimental, as of libQuotient 0.7) - either Open Source or + Commercial +- CMake 3.16 or newer +- A C++ toolchain that supports at least some subset of C++20 (concepts, + in particular): - GCC 11 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) and Visual Studio 2019 (Windows) are the oldest officially supported. +- If using E2EE (beta, as of libQuotient 0.7): + - libolm 3.x (the latest 3.x strongly recommended) + - OpenSSL (1.1.x is known to work; 3.x should likely work too). - Any build system that works with CMake should be fine: GNU Make and ninja on any platform, NMake and jom on Windows are known to work. Ninja is recommended. @@ -56,8 +60,11 @@ at error messages. The library is entirely offscreen but aside from QtCore and QtNetwork it also depends on QtGui in order to handle avatar thumbnails. #### macOS -`brew install qt5` should get you a recent Qt5. You may need to pass -`-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` to make it aware of the Qt location. +`brew install qt5` should get you a recent Qt5. You may need to add the output +of `brew --prefix qt5` to `CMAKE_PREFIX_PATH` (see below) to make CMake aware +of the Qt location. + +If using E2EE, you need to perform the same dance for libolm and openssl. #### Windows Install Qt5 using their official installer; make sure to tick the CMake box @@ -72,6 +79,14 @@ on system startup but it's very handy to setup the environment before building. Alternatively you can add the Qt path to `CMAKE_PREFIX_PATH` and leave PATH unchanged. +If you're trying out E2EE, you will also need libolm and OpenSSL. Unfortunately, +neither project provides official binary libraries for Windows. libolm can +be compiled from the sources (available at ) using the same toolchain +(CMake+MSVC). It's not recommended to compile OpenSSL yourself; instead, use +one of the "OpenSSL for Windows" links in +[unofficial list on the project Wiki](https://wiki.openssl.org/index.php/Binaries). + + ### Using the library If you're just starting a project using libQuotient from scratch, you can copy `quotest/CMakeLists.txt` to your project and change `quotest` to your @@ -121,6 +136,9 @@ Static builds are tested on all supported platforms, building the library as a shared object (aka dynamic library) is supported on Linux and macOS but is very likely to be broken on Windows. +Before proceeding, double-check that you have installed development libraries +for all prerequisites above. CMake will stop and tell you if something's missing. + The first CMake invocation above configures the build. You can pass CMake variables (such as `-DCMAKE_PREFIX_PATH="path1;path2;..."` and `-DCMAKE_INSTALL_PREFIX=path`) here if needed. @@ -137,11 +155,14 @@ the standard variables coming with CMake. On top of them, Quotient introduces: to get the contents of `#quotient:matrix.org`; this is being fixed in [#401](https://github.com/quotient-im/libQuotient/issues/401). - `Quotient_ENABLE_E2EE=`, `OFF` by default - enable work-in-progress - E2EE code in the library. As of 0.6, this code is very incomplete and leaks - memory; only set this to `ON` if you want to help making this code work. + E2EE code in the library. As of 0.6, this code is very incomplete and buggy; + you should NEVER use it. In 0.7, the enabled code is beta-quality and is + generally good for trying the technology and API but really not for + mission-critical applications. + Switching this on will define `Quotient_E2EE_ENABLED` macro (note the difference from the CMake switch) for compiler invocations on all - Quotient and Quotient-dependent (if it uses `find_package(Quotient 0.6)`) + Quotient and Quotient-dependent (if it uses `find_package(Quotient)`) code; so you can use `#ifdef Quotient_E2EE_ENABLED` to guard the code using E2EE parts of Quotient. - `MATRIX_SPEC_PATH` and `GTAD_PATH` - these two variables are used to point -- cgit v1.2.3 From b1de80b4169d5f60f0f1f7fd20018ba6521c293e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 17 Sep 2022 21:22:40 +0200 Subject: Cleanup NAM more carefully This tries to fix getting stuck at exit since NAM has been switched from QThreadStorage to thread_local --- lib/networkaccessmanager.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 78be89bd..44a306d1 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -9,6 +9,7 @@ #include "mxcreply.h" #include +#include #include #include @@ -68,8 +69,13 @@ void NetworkAccessManager::clearIgnoredSslErrors() NetworkAccessManager* NetworkAccessManager::instance() { - thread_local NetworkAccessManager nam; - return &nam; + thread_local auto* nam = [] { + auto* namInit = new NetworkAccessManager(); + connect(QThread::currentThread(), &QThread::finished, namInit, + &QObject::deleteLater); + return namInit; + }(); + return nam; } QNetworkReply* NetworkAccessManager::createRequest( -- cgit v1.2.3 From 82e23011e0336a719fe3f2c003496d112bc5ca65 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 20 Sep 2022 23:20:53 +0200 Subject: .clang-tidy: drop readability-qualified-auto Too many suggestions to add, e.g., * in places where it may eventually be a smart pointer. [skip ci] --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index d15c6eb4..db460c99 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,-misc-definitions-in-headers,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,-performance-no-automatic-move,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' +Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,-misc-definitions-in-headers,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,-performance-no-automatic-move,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' WarningsAsErrors: '' HeaderFilterRegex: '' AnalyzeTemporaryDtors: false -- cgit v1.2.3 From a0cc4eaf6af6f047e32713b2926f784fa4087a64 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 17 Sep 2022 13:21:35 +0200 Subject: Bump required Olm version to 3.2.5 This is the earliest version shipping olm/error.h. Conversely, stock libolm that comes with Ubuntu 20.04 (version 3.1.3) is no more enough so CI completely switches to jammy now. --- .github/workflows/ci.yml | 8 ++++---- CMakeLists.txt | 2 +- README.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f84356b0..1c0940eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false max-parallel: 1 matrix: - os: [ ubuntu-20.04, macos-11 ] + os: [ ubuntu-22.04, macos-11 ] qt-version: [ '6.3.1', '5.15.2' ] compiler: [ LLVM ] # Not using binary values here, to make the job captions more readable @@ -31,7 +31,7 @@ jobs: exclude: - qt-version: '6.3.1' update-api: update-api # Generated code is not specific to Qt version - - os: ubuntu-20.04 + - os: ubuntu-22.04 e2ee: e2ee # Will be re-added with static analysis below # TODO: Enable E2EE on Windows and macOS - os: macos-11 @@ -42,7 +42,7 @@ jobs: compiler: MSVC platform: x64 qt-arch: win64_msvc2019_64 - - os: ubuntu-20.04 + - os: ubuntu-22.04 qt-version: '5.15.2' compiler: LLVM e2ee: e2ee @@ -57,7 +57,7 @@ jobs: compiler: GCC e2ee: e2ee update-api: update-api - - os: ubuntu-20.04 + - os: ubuntu-22.04 qt-version: '5.15.2' compiler: LLVM update-api: update-api diff --git a/CMakeLists.txt b/CMakeLists.txt index 91ee8217..a56115e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,7 +101,7 @@ find_package(${Qt}Keychain REQUIRED) if (${PROJECT_NAME}_ENABLE_E2EE) find_package(${Qt} ${QtMinVersion} REQUIRED Sql) - find_package(Olm 3.1.3 REQUIRED) + find_package(Olm 3.2.5 REQUIRED) set_package_properties(Olm PROPERTIES DESCRIPTION "Implementation of the Olm and Megolm cryptographic ratchets" URL "https://gitlab.matrix.org/matrix-org/olm" diff --git a/README.md b/README.md index e7597fce..d9f50f0a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ your application, as described below. - GCC 11 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) and Visual Studio 2019 (Windows) are the oldest officially supported. - If using E2EE (beta, as of libQuotient 0.7): - - libolm 3.x (the latest 3.x strongly recommended) + - libolm 3.2.5 or newer (the latest 3.x strongly recommended) - OpenSSL (1.1.x is known to work; 3.x should likely work too). - Any build system that works with CMake should be fine: GNU Make and ninja on any platform, NMake and jom on Windows are known to work. -- cgit v1.2.3 From a923750c7a1efadaa66f24dc17010063776e6246 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 18 Sep 2022 13:04:41 +0200 Subject: CI: only use Valgrind with GCC As of the current GHA jammy image, Valgrind cannot cope with some of Clang 14 output: https://bugs.kde.org/show_bug.cgi?id=452758 This is most likely a band-aid, before the work on switching from Valgrind to ASan. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c0940eb..40ed85d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,7 @@ jobs: sed -i 's/ThreadEngineStarter(ThreadEngine \*_threadEngine)/ThreadEngineStarter(ThreadEngine \*_threadEngine)/' \ $Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h fi + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV elif [[ '${{ runner.os }}' != 'Windows' ]]; then echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV @@ -155,7 +156,6 @@ jobs: echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV fi sudo apt-get -qq install ninja-build valgrind $EXTRA_DEPS - echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 -- cgit v1.2.3 From 94d1bb2c624ca9a689a5d7cdca8af6ef5e489150 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 22 Sep 2022 20:41:26 +0200 Subject: Don't crash in MxcReply if the event is not a RoomMessageEvent --- lib/mxcreply.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index c7547be8..ce833b98 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -63,10 +63,11 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) #ifdef Quotient_E2EE_ENABLED auto eventIt = room->findInTimeline(eventId); if(eventIt != room->historyEdge()) { - auto event = eventIt->viewAs(); - if (auto* efm = std::get_if( - &event->content()->fileInfo()->source)) - d->m_encryptedFile = *efm; + if (auto event = eventIt->viewAs()) { + if (auto* efm = std::get_if( + &event->content()->fileInfo()->source)) + d->m_encryptedFile = *efm; + } } #endif } -- cgit v1.2.3 From f161519740da12c2578d6ecf79a501e10c2debae Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 24 Sep 2022 11:36:44 +0200 Subject: Fix construction of EncryptedEvents The parent constructor requires full json instead of content json now --- lib/events/encryptedevent.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 94b44901..9ef3b22a 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -8,20 +8,28 @@ using namespace Quotient; EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertexts, const QString& senderKey) - : RoomEvent({ { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, - { CiphertextKeyL, ciphertexts }, - { SenderKeyKeyL, senderKey } }) + : RoomEvent({ + {"content", QJsonObject{ + { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, + { CiphertextKeyL, ciphertexts }, + { SenderKeyKeyL, senderKey } + }}, + {TypeKeyL, "m.room.encrypted"} + }) {} EncryptedEvent::EncryptedEvent(const QByteArray& ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId) : RoomEvent({ - { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, - { CiphertextKeyL, QString(ciphertext) }, - { DeviceIdKeyL, deviceId }, - { SenderKeyKeyL, senderKey }, - { SessionIdKeyL, sessionId }, + {"content", QJsonObject{ + { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL, QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId }, + }}, + {TypeKeyL, "m.room.encrypted"} }) {} -- cgit v1.2.3 From a2b155b10197a4fca4db2af59c2171f82a0eac2b Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 24 Sep 2022 12:15:54 +0200 Subject: Fix verification Contains two fixes: - When receiving the mac, we can also be in WAITINGFORVERIFICATION state - Ignore all KeyVerificationDone events; we don't do anything with them anyway and sometimes receive them after the session is destructed --- lib/connection.cpp | 4 ++++ lib/keyverificationsession.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 8ca76ceb..307c3840 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1006,6 +1006,10 @@ bool Connection::Private::processIfVerificationEvent(const Event& evt, emit q->newKeyVerificationSession(*sessionIter); return true; }, + [](const KeyVerificationDoneEvent& doneEvt) { + Q_UNUSED(doneEvt) + return true; + }, [this](const KeyVerificationEvent& kvEvt) { if (auto* const session = verificationSessions.value(kvEvt.transactionId())) { diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 3f76eac1..f5d49561 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -120,7 +120,7 @@ void KeyVerificationSession::handleEvent(const KeyVerificationEvent& baseEvent) return true; }, [this](const KeyVerificationMacEvent& event) { - if (state() != WAITINGFORMAC) + if (state() != WAITINGFORMAC && state() != WAITINGFORVERIFICATION) return false; handleMac(event); return true; -- cgit v1.2.3 From 4146f33bda60d04db34fbd5614439b22b5d5837f Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 24 Sep 2022 14:27:16 +0200 Subject: Use basicJson() --- lib/events/encryptedevent.cpp | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 9ef3b22a..49df25c8 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -8,29 +8,19 @@ using namespace Quotient; EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertexts, const QString& senderKey) - : RoomEvent({ - {"content", QJsonObject{ - { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, - { CiphertextKeyL, ciphertexts }, - { SenderKeyKeyL, senderKey } - }}, - {TypeKeyL, "m.room.encrypted"} - }) + : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, + { CiphertextKeyL, ciphertexts }, + { SenderKeyKeyL, senderKey } })) {} EncryptedEvent::EncryptedEvent(const QByteArray& ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId) - : RoomEvent({ - {"content", QJsonObject{ - { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, - { CiphertextKeyL, QString(ciphertext) }, - { DeviceIdKeyL, deviceId }, - { SenderKeyKeyL, senderKey }, - { SessionIdKeyL, sessionId }, - }}, - {TypeKeyL, "m.room.encrypted"} - }) + : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL, QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId } })) {} EncryptedEvent::EncryptedEvent(const QJsonObject& obj) -- cgit v1.2.3 From 63d658e798c1ba29d080564db06a613a3d7d5df5 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:22:05 +0200 Subject: Update lib/connection.cpp Co-authored-by: Alexey Rusakov --- lib/connection.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 307c3840..1048884f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1006,8 +1006,7 @@ bool Connection::Private::processIfVerificationEvent(const Event& evt, emit q->newKeyVerificationSession(*sessionIter); return true; }, - [](const KeyVerificationDoneEvent& doneEvt) { - Q_UNUSED(doneEvt) + [](const KeyVerificationDoneEvent&) { return true; }, [this](const KeyVerificationEvent& kvEvt) { -- cgit v1.2.3 From 5904a61c59f0eef00aef07ef998658fd791ff139 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 25 Sep 2022 21:37:49 +0200 Subject: QOlmUtility::ed25519Verify: just return bool It's too easy to incorrectly test the previous return type. --- autotests/testolmaccount.cpp | 3 +-- autotests/testolmutility.cpp | 6 ++---- lib/e2ee/qolmaccount.cpp | 3 +-- lib/e2ee/qolmutility.cpp | 23 +++++------------------ lib/e2ee/qolmutility.h | 4 ++-- 5 files changed, 11 insertions(+), 28 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 280705d0..a41af268 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -58,8 +58,7 @@ void TestOlmAccount::signatureValid() QOlmUtility utility; const auto identityKeys = olmAccount.identityKeys(); const auto ed25519Key = identityKeys.ed25519; - const auto verify = utility.ed25519Verify(ed25519Key, message, signature); - QVERIFY(verify.value_or(false)); + QVERIFY(utility.ed25519Verify(ed25519Key, message, signature)); } void TestOlmAccount::oneTimeKeysValid() diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 5b67c805..1d461a94 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -81,10 +81,8 @@ void TestOlmUtility::verifySignedOneTimeKey() delete[](reinterpret_cast(utility)); QOlmUtility utility2; - auto res2 = - utility2 - .ed25519Verify(aliceOlm.identityKeys().ed25519, msg, signatureBuf1) - .value_or(false); + auto res2 = utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg, + signatureBuf1); //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); QVERIFY(res2); diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index ccb191f4..1b04dae7 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -264,6 +264,5 @@ bool Quotient::ed25519VerifySignature(const QString& signingKey, QByteArray signingKeyBuf = signingKey.toUtf8(); QOlmUtility utility; auto signatureBuf = signature.toUtf8(); - return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf) - .value_or(false); + return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); } diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 84559085..22f9ec0d 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -40,23 +40,10 @@ QString QOlmUtility::sha256Utf8Msg(const QString &message) const return sha256Bytes(message.toUtf8()); } -QOlmExpected QOlmUtility::ed25519Verify(const QByteArray& key, - const QByteArray& message, - const QByteArray& signature) +bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message, + QByteArray signature) { - QByteArray signatureBuf(signature.length(), '0'); - std::copy(signature.begin(), signature.end(), signatureBuf.begin()); - - const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), - message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); - - if (ret == olm_error()) { - auto error = lastError(m_utility); - if (error == QOlmError::BadMessageMac) { - return false; - } - return error; - } - - return !ret; // ret == 0 means success + return olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), + message.size(), signature.data(), signature.size()) + == 0; } diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index 5f6bcdc5..6c1c8624 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -29,8 +29,8 @@ public: //! \param key QByteArray The public part of the ed25519 key that signed the message. //! \param message QByteArray The message that was signed. //! \param signature QByteArray The signature of the message. - QOlmExpected ed25519Verify(const QByteArray &key, - const QByteArray &message, const QByteArray &signature); + bool ed25519Verify(const QByteArray &key, + const QByteArray &message, QByteArray signature); private: OlmUtility *m_utility; -- 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. --- CMakeLists.txt | 2 +- autotests/testolmaccount.cpp | 3 +- autotests/testolmsession.cpp | 8 +++- lib/connection.cpp | 3 +- lib/e2ee/e2ee.h | 36 ++++----------- lib/e2ee/qolmaccount.cpp | 82 +++++++++++++++++---------------- lib/e2ee/qolmaccount.h | 9 ++-- lib/e2ee/qolmerrors.cpp | 25 ---------- lib/e2ee/qolmerrors.h | 28 ------------ lib/e2ee/qolminboundsession.cpp | 84 +++++++++++++++++++--------------- lib/e2ee/qolminboundsession.h | 5 +- lib/e2ee/qolmoutboundsession.cpp | 81 +++++++++++++++++++-------------- lib/e2ee/qolmoutboundsession.h | 7 ++- lib/e2ee/qolmsession.cpp | 98 +++++++++++++++++++++------------------- lib/e2ee/qolmsession.h | 4 +- lib/e2ee/qolmutility.cpp | 22 +++++---- lib/e2ee/qolmutility.h | 4 +- lib/events/encryptedevent.cpp | 1 + lib/events/encryptedevent.h | 7 ++- lib/room.cpp | 1 - lib/util.h | 17 +++++++ 21 files changed, 266 insertions(+), 261 deletions(-) delete mode 100644 lib/e2ee/qolmerrors.cpp delete mode 100644 lib/e2ee/qolmerrors.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a56115e3..b021411c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ list(APPEND lib_SRCS lib/eventitem.h lib/eventitem.cpp lib/accountregistry.h lib/accountregistry.cpp lib/mxcreply.h lib/mxcreply.cpp + lib/e2ee/e2ee.h # because it's used by generated API lib/events/event.h lib/events/event.cpp lib/events/eventloader.h lib/events/roomevent.h lib/events/roomevent.cpp @@ -193,7 +194,6 @@ if (${PROJECT_NAME}_ENABLE_E2EE) lib/e2ee/qolmoutboundsession.h lib/e2ee/qolmoutboundsession.cpp lib/e2ee/qolmutils.h lib/e2ee/qolmutils.cpp lib/e2ee/qolmutility.h lib/e2ee/qolmutility.cpp - lib/e2ee/qolmerrors.h lib/e2ee/qolmerrors.cpp lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp lib/e2ee/qolmmessage.h lib/e2ee/qolmmessage.cpp lib/events/keyverificationevent.h diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 280705d0..eb428661 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -25,7 +25,8 @@ void TestOlmAccount::pickleUnpickledTest() auto identityKeys = olmAccount.identityKeys(); auto pickled = olmAccount.pickle(Unencrypted{}).value(); QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); - olmAccount2.unpickle(pickled, Unencrypted{}); + auto unpickleResult = olmAccount2.unpickle(pickled, Unencrypted{}); + QCOMPARE(unpickleResult, 0); auto identityKeys2 = olmAccount2.identityKeys(); QCOMPARE(identityKeys.curve25519, identityKeys2.curve25519); QCOMPARE(identityKeys.ed25519, identityKeys2.ed25519); diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 182659e7..66a04241 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -12,9 +12,13 @@ std::pair createSessionPair() QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr"); auto accountA = QOlmAccount("accountA:foo.com", "Device1UserA"); - accountA.unpickle(pickledAccountA, Unencrypted{}); + if (accountA.unpickle(pickledAccountA, Unencrypted{}) != OLM_SUCCESS) + qFatal("Failed to unpickle account A: %s", accountA.lastError()); + auto accountB = QOlmAccount("accountB:foo.com", "Device1UserB"); - accountB.unpickle(pickledAccountB, Unencrypted{}); + if (accountB.unpickle(pickledAccountB, Unencrypted{}) != OLM_SUCCESS) + qFatal("Failed to unpickle account B: %s", accountB.lastError()); + const QByteArray identityKeyA("qIEr3TWcJQt4CP8QoKKJcCaukByIOpgh6erBkhLEa2o"); const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk"); diff --git a/lib/connection.cpp b/lib/connection.cpp index 8ca76ceb..865dff79 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -270,8 +270,7 @@ public: return {}; } auto newSession = std::move(*newSessionResult); - auto error = olmAccount->removeOneTimeKeys(*newSession); - if (error) { + if (olmAccount->removeOneTimeKeys(*newSession) != OLM_SUCCESS) { qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); // Keep going though diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 0772b70a..51ceff67 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -6,21 +6,20 @@ #pragma once #include "converters.h" -#include "expected.h" -#include "qolmerrors.h" #include #include #include -#include -namespace Quotient { +#ifdef Quotient_E2EE_ENABLED +# include "expected.h" + +# include +# include +#endif -constexpr auto CiphertextKeyL = "ciphertext"_ls; -constexpr auto SenderKeyKeyL = "sender_key"_ls; -constexpr auto DeviceIdKeyL = "device_id"_ls; -constexpr auto SessionIdKeyL = "session_id"_ls; +namespace Quotient { constexpr auto AlgorithmKeyL = "algorithm"_ls; constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; @@ -47,6 +46,7 @@ inline bool isSupportedAlgorithm(const QString& algorithm) != SupportedAlgorithms.cend(); } +#ifdef Quotient_E2EE_ENABLED struct Unencrypted {}; struct Encrypted { QByteArray key; @@ -64,7 +64,8 @@ class QOlmOutboundGroupSession; using QOlmOutboundGroupSessionPtr = std::unique_ptr; template -using QOlmExpected = Expected; +using QOlmExpected = Expected; +#endif struct IdentityKeys { @@ -133,23 +134,6 @@ private: using OneTimeKeys = QHash>; -template -class asKeyValueRange -{ -public: - asKeyValueRange(T& data) - : m_data { data } - {} - - auto begin() { return m_data.keyValueBegin(); } - auto end() { return m_data.keyValueEnd(); } - -private: - T &m_data; -}; -template -asKeyValueRange(T&) -> asKeyValueRange; - } // namespace Quotient Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index ccb191f4..d33db8e3 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -18,8 +18,13 @@ using namespace Quotient; // Convert olm error to enum -QOlmError lastError(OlmAccount *account) { - return fromString(olm_account_last_error(account)); +OlmErrorCode QOlmAccount::lastErrorCode() const { + return olm_account_last_error_code(m_account); +} + +const char* QOlmAccount::lastError() const +{ + return olm_account_last_error(m_account); } QOlmAccount::QOlmAccount(const QString& userId, const QString& deviceId, @@ -40,24 +45,24 @@ void QOlmAccount::createNewAccount() m_account = olm_account(new uint8_t[olm_account_size()]); size_t randomSize = olm_create_account_random_length(m_account); QByteArray randomData = getRandom(randomSize); - const auto error = olm_create_account(m_account, randomData.data(), randomSize); - if (error == olm_error()) { - throw lastError(m_account); + if (olm_create_account(m_account, randomData.data(), randomSize) + == olm_error()) { + throw lastError(); } emit needsSave(); } -void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) +OlmErrorCode QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) { m_account = olm_account(new uint8_t[olm_account_size()]); const QByteArray key = toKey(mode); - const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()); - if (error == olm_error()) { - qCWarning(E2EE) << "Failed to unpickle olm account"; - //TODO: Do something that is not dying + if (olm_unpickle_account(m_account, key.data(), key.length(), + pickled.data(), pickled.size()) + == olm_error()) { // Probably log the user out since we have no way of getting to the keys - //throw lastError(m_account); + return lastErrorCode(); } + return OLM_SUCCESS; } QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) @@ -65,11 +70,10 @@ QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); QByteArray pickleBuffer(pickleLength, '0'); - const auto error = olm_pickle_account(m_account, key.data(), - key.length(), pickleBuffer.data(), pickleLength); - if (error == olm_error()) { - return lastError(m_account); - } + if (olm_pickle_account(m_account, key.data(), key.length(), + pickleBuffer.data(), pickleLength) + == olm_error()) + return lastErrorCode(); return pickleBuffer; } @@ -77,9 +81,9 @@ IdentityKeys QOlmAccount::identityKeys() const { const size_t keyLength = olm_account_identity_keys_length(m_account); QByteArray keyBuffer(keyLength, '0'); - const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength); - if (error == olm_error()) { - throw lastError(m_account); + if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength) + == olm_error()) { + throw lastError(); } const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); return IdentityKeys { @@ -92,11 +96,10 @@ QByteArray QOlmAccount::sign(const QByteArray &message) const { QByteArray signatureBuffer(olm_account_signature_length(m_account), '0'); - const auto error = olm_account_sign(m_account, message.data(), message.length(), - signatureBuffer.data(), signatureBuffer.length()); - - if (error == olm_error()) { - throw lastError(m_account); + if (olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureBuffer.length()) + == olm_error()) { + throw lastError(); } return signatureBuffer; } @@ -131,15 +134,15 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); QByteArray randomBuffer = getRandom(randomLength); - const auto error = + const auto result = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); - if (error == olm_error()) { - throw lastError(m_account); + if (result == olm_error()) { + throw lastError(); } emit needsSave(); - return error; + return result; } UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const @@ -147,11 +150,10 @@ UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); QByteArray oneTimeKeysBuffer(static_cast(oneTimeKeyLength), '0'); - const auto error = olm_account_one_time_keys(m_account, - oneTimeKeysBuffer.data(), - oneTimeKeyLength); - if (error == olm_error()) { - throw lastError(m_account); + if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), + oneTimeKeyLength) + == olm_error()) { + throw lastError(); } const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); UnsignedOneTimeKeys oneTimeKeys; @@ -171,16 +173,16 @@ OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const return signedOneTimeKeys; } -std::optional QOlmAccount::removeOneTimeKeys( - const QOlmSession& session) +OlmErrorCode QOlmAccount::removeOneTimeKeys(const QOlmSession& session) { - const auto error = olm_remove_one_time_keys(m_account, session.raw()); - - if (error == olm_error()) { - return lastError(m_account); + if (olm_remove_one_time_keys(m_account, session.raw()) == olm_error()) { + qWarning(E2EE).nospace() + << "Failed to remove one-time keys for session " + << session.sessionId() << ": " << lastError(); + return lastErrorCode(); } emit needsSave(); - return std::nullopt; + return OLM_SUCCESS; } OlmAccount* QOlmAccount::data() { return m_account; } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index f2a31314..5ad98e47 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -36,7 +36,8 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. //! This needs to be called before any other action or use createNewAccount() instead. - void unpickle(QByteArray &pickled, const PicklingMode &mode); + [[nodiscard]] OlmErrorCode unpickle(QByteArray& pickled, + const PicklingMode& mode); //! Serialises an OlmAccount to encrypted Base64. QOlmExpected pickle(const PicklingMode &mode); @@ -69,8 +70,7 @@ public: DeviceKeys deviceKeys() const; //! Remove the one time key used to create the supplied session. - [[nodiscard]] std::optional removeOneTimeKeys( - const QOlmSession& session); + [[nodiscard]] OlmErrorCode removeOneTimeKeys(const QOlmSession& session); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! @@ -92,6 +92,9 @@ public: void markKeysAsPublished(); + OlmErrorCode lastErrorCode() const; + const char *lastError() const; + // HACK do not use directly QOlmAccount(OlmAccount *account); OlmAccount *data(); diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp deleted file mode 100644 index 5a60b7e6..00000000 --- a/lib/e2ee/qolmerrors.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - - -#include "qolmerrors.h" -#include "util.h" -#include - -Quotient::QOlmError Quotient::fromString(const char* error_raw) { - const QLatin1String error { error_raw }; - if (error_raw == "BAD_ACCOUNT_KEY"_ls) { - return QOlmError::BadAccountKey; - } else if (error_raw == "BAD_MESSAGE_KEY_ID"_ls) { - return QOlmError::BadMessageKeyId; - } else if (error_raw == "INVALID_BASE64"_ls) { - return QOlmError::InvalidBase64; - } else if (error_raw == "NOT_ENOUGH_RANDOM"_ls) { - return QOlmError::NotEnoughRandom; - } else if (error_raw == "OUTPUT_BUFFER_TOO_SMALL"_ls) { - return QOlmError::OutputBufferTooSmall; - } else { - return QOlmError::Unknown; - } -} diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h deleted file mode 100644 index 20e61c12..00000000 --- a/lib/e2ee/qolmerrors.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include "quotient_export.h" - -namespace Quotient { -//! All errors that could be caused by an operation regarding Olm -//! Errors are named exactly like the ones in libolm. -enum QOlmError -{ - BadAccountKey, - BadMessageFormat, - BadMessageKeyId, - BadMessageMac, - BadMessageVersion, - InvalidBase64, - NotEnoughRandom, - OutputBufferTooSmall, - UnknownMessageIndex, - Unknown, -}; - -QUOTIENT_API QOlmError fromString(const char* error_raw); - -} //namespace Quotient diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 17f06205..870070c2 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -3,20 +3,27 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "e2ee/qolminboundsession.h" -#include +#include "logging.h" + #include +#include +#include using namespace Quotient; -QOlmError lastError(OlmInboundGroupSession *session) { - return fromString(olm_inbound_group_session_last_error(session)); +OlmErrorCode QOlmInboundGroupSession::lastErrorCode() const { + return olm_inbound_group_session_last_error_code(m_groupSession); } -QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) - : m_groupSession(session) +const char* QOlmInboundGroupSession::lastError() const { + return olm_inbound_group_session_last_error(m_groupSession); } +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) + : m_groupSession(session) +{} + QOlmInboundGroupSession::~QOlmInboundGroupSession() { olm_clear_inbound_group_session(m_groupSession); @@ -26,11 +33,12 @@ QOlmInboundGroupSession::~QOlmInboundGroupSession() std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - const auto error = olm_init_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(key.constData()), key.size()); - - if (error == olm_error()) { - throw lastError(olmInboundGroupSession); + if (olm_init_inbound_group_session( + olmInboundGroupSession, + reinterpret_cast(key.constData()), key.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + throw olm_inbound_group_session_last_error_code(olmInboundGroupSession); } return std::make_unique(olmInboundGroupSession); @@ -41,10 +49,12 @@ std::unique_ptr QOlmInboundGroupSession::import(const Q const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); QByteArray keyBuf = key; - const auto error = olm_import_inbound_group_session(olmInboundGroupSession, - reinterpret_cast(keyBuf.data()), keyBuf.size()); - if (error == olm_error()) { - throw lastError(olmInboundGroupSession); + if (olm_import_inbound_group_session( + olmInboundGroupSession, + reinterpret_cast(keyBuf.data()), keyBuf.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + throw olm_inbound_group_session_last_error_code(olmInboundGroupSession); } return std::make_unique(olmInboundGroupSession); @@ -62,10 +72,11 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); const QByteArray key = toKey(mode); - const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), - pickledBuf.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); + if (olm_pickle_inbound_group_session(m_groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) { + throw lastError(); } return pickledBuf; } @@ -76,10 +87,12 @@ QOlmExpected QOlmInboundGroupSession::unpickle( QByteArray pickledBuf = pickled; const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); QByteArray key = toKey(mode); - const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.size()); - if (error == olm_error()) { - return lastError(groupSession); + if (olm_unpickle_inbound_group_session(groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + return olm_inbound_group_session_last_error_code(groupSession); } key.clear(); @@ -105,12 +118,9 @@ QOlmExpected> QOlmInboundGroupSession::decrypt( const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), messageBuf.length(), reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); - - // Error code or plaintext length is returned - const auto decryptError = plaintextLen; - - if (decryptError == olm_error()) { - return lastError(m_groupSession); + if (plaintextLen == olm_error()) { + qWarning(E2EE) << "Failed to decrypt the message:" << lastError(); + return lastErrorCode(); } QByteArray output(plaintextLen, '0'); @@ -123,10 +133,11 @@ QOlmExpected QOlmInboundGroupSession::exportSession(uint32_t message { const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); QByteArray keyBuf(keyLength, '0'); - const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast(keyBuf.data()), keyLength, messageIndex); - - if (error == olm_error()) { - return lastError(m_groupSession); + if (olm_export_inbound_group_session( + m_groupSession, reinterpret_cast(keyBuf.data()), + keyLength, messageIndex) + == olm_error()) { + return lastErrorCode(); } return keyBuf; } @@ -139,10 +150,11 @@ uint32_t QOlmInboundGroupSession::firstKnownIndex() const QByteArray QOlmInboundGroupSession::sessionId() const { QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); - const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast(sessionIdBuf.data()), - sessionIdBuf.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); + if (olm_inbound_group_session_id( + m_groupSession, reinterpret_cast(sessionIdBuf.data()), + sessionIdBuf.length()) + == olm_error()) { + throw lastError(); } return sessionIdBuf; } diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 1a9b4415..e8da6355 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -6,7 +6,7 @@ #include "e2ee/e2ee.h" -#include +struct OlmInboundGroupSession; namespace Quotient { @@ -46,6 +46,9 @@ public: QString senderId() const; void setSenderId(const QString& senderId); + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + QOlmInboundGroupSession(OlmInboundGroupSession* session); private: OlmInboundGroupSession* m_groupSession; diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index a2eff2c8..79c16e01 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -3,12 +3,20 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "e2ee/qolmoutboundsession.h" + #include "e2ee/qolmutils.h" +#include + using namespace Quotient; -QOlmError lastError(OlmOutboundGroupSession *session) { - return fromString(olm_outbound_group_session_last_error(session)); +OlmErrorCode QOlmOutboundGroupSession::lastErrorCode() const { + return olm_outbound_group_session_last_error_code(m_groupSession); +} + +const char* QOlmOutboundGroupSession::lastError() const +{ + return olm_outbound_group_session_last_error(m_groupSession); } QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) @@ -27,11 +35,12 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); QByteArray randomBuf = getRandom(randomLength); - const auto error = olm_init_outbound_group_session(olmOutboundGroupSession, - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - throw lastError(olmOutboundGroupSession); + if (olm_init_outbound_group_session( + olmOutboundGroupSession, + reinterpret_cast(randomBuf.data()), randomBuf.length()) + == olm_error()) { + // FIXME: create the session object earlier and use lastError() + throw olm_outbound_group_session_last_error_code(olmOutboundGroupSession); } const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); @@ -48,12 +57,11 @@ QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mo { QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); QByteArray key = toKey(mode); - const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_groupSession); - } + if (olm_pickle_outbound_group_session(m_groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) + return lastErrorCode(); key.clear(); @@ -65,10 +73,13 @@ QOlmExpected QOlmOutboundGroupSession::unpickle(con QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); QByteArray key = toKey(mode); - const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - if (error == olm_error()) { - return lastError(olmOutboundGroupSession); + if (olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) { + // FIXME: create the session object earlier and use lastError() + return olm_outbound_group_session_last_error_code( + olmOutboundGroupSession); } const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); QByteArray idBuffer(idMaxLength, '0'); @@ -84,12 +95,13 @@ QOlmExpected QOlmOutboundGroupSession::encrypt(const QString &plaint QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); QByteArray messageBuf(messageMaxLength, '0'); - const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast(plaintextBuf.data()), - plaintextBuf.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - return lastError(m_groupSession); - } + if (olm_group_encrypt(m_groupSession, + reinterpret_cast(plaintextBuf.data()), + plaintextBuf.length(), + reinterpret_cast(messageBuf.data()), + messageBuf.length()) + == olm_error()) + return lastErrorCode(); return messageBuf; } @@ -103,11 +115,12 @@ QByteArray QOlmOutboundGroupSession::sessionId() const { const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_groupSession); - } + if (olm_outbound_group_session_id( + m_groupSession, reinterpret_cast(idBuffer.data()), + idBuffer.length()) + == olm_error()) + throw lastError(); + return idBuffer; } @@ -115,12 +128,12 @@ QOlmExpected QOlmOutboundGroupSession::sessionKey() const { const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); QByteArray keyBuffer(keyMaxLength, '0'); - const auto error = olm_outbound_group_session_key( - m_groupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); - if (error == olm_error()) { - return lastError(m_groupSession); - } + if (olm_outbound_group_session_key( + m_groupSession, reinterpret_cast(keyBuffer.data()), + keyMaxLength) + == olm_error()) + return lastErrorCode(); + return keyBuffer; } diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 9a82d22a..cd26fc67 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -6,8 +6,7 @@ #include "e2ee/e2ee.h" -#include -#include +struct OlmOutboundGroupSession; namespace Quotient { @@ -51,6 +50,10 @@ public: QDateTime creationTime() const; void setCreationTime(const QDateTime& creationTime); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + private: OlmOutboundGroupSession *m_groupSession; int m_messageCount = 0; diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 2a98d5d8..771d310d 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -12,8 +12,13 @@ using namespace Quotient; -QOlmError lastError(OlmSession* session) { - return fromString(olm_session_last_error(session)); +OlmErrorCode QOlmSession::lastErrorCode() const { + return olm_session_last_error_code(m_session); +} + +const char* QOlmSession::lastError() const +{ + return olm_session_last_error(m_session); } Quotient::QOlmSession::~QOlmSession() @@ -32,7 +37,8 @@ QOlmExpected QOlmSession::createInbound( const QString& theirIdentityKey) { if (preKeyMessage.type() != QOlmMessage::PreKey) { - qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat; + qCCritical(E2EE) << "The message is not a pre-key; will try to create " + "the inbound session anyway"; } const auto olmSession = create(); @@ -47,7 +53,8 @@ QOlmExpected QOlmSession::createInbound( } if (error == olm_error()) { - const auto lastErr = lastError(olmSession); + // FIXME: the QOlmSession object should be created earlier + const auto lastErr = olm_session_last_error_code(olmSession); qCWarning(E2EE) << "Error when creating inbound session" << lastErr; return lastErr; } @@ -78,15 +85,17 @@ QOlmExpected QOlmSession::createOutboundSession( QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); - const auto error = olm_create_outbound_session(olmOutboundSession, - account->data(), - reinterpret_cast(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(), - reinterpret_cast(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length()); - - if (error == olm_error()) { - const auto lastErr = lastError(olmOutboundSession); - if (lastErr == QOlmError::NotEnoughRandom) { + if (olm_create_outbound_session( + olmOutboundSession, account->data(), + reinterpret_cast(theirIdentityKeyBuf.data()), + theirIdentityKeyBuf.length(), + reinterpret_cast(theirOneTimeKeyBuf.data()), + theirOneTimeKeyBuf.length(), + reinterpret_cast(randomBuf.data()), randomBuf.length()) + == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto lastErr = olm_session_last_error_code(olmOutboundSession); + if (lastErr == OLM_NOT_ENOUGH_RANDOM) { throw lastErr; } return lastErr; @@ -100,16 +109,12 @@ QOlmExpected QOlmSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); QByteArray key = toKey(mode); - const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), - pickledBuf.length()); - - if (error == olm_error()) { - return lastError(m_session); - } + if (olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()) + == olm_error()) + return lastErrorCode(); key.clear(); - return pickledBuf; } @@ -119,10 +124,11 @@ QOlmExpected QOlmSession::unpickle(const QByteArray& pickled, QByteArray pickledBuf = pickled; auto *olmSession = create(); QByteArray key = toKey(mode); - const auto error = olm_unpickle_session(olmSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); - if (error == olm_error()) { - return lastError(olmSession); + if (olm_unpickle_session(olmSession, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()) + == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + return olm_session_last_error_code(olmSession); } key.clear(); @@ -137,13 +143,14 @@ QOlmMessage QOlmSession::encrypt(const QString &plaintext) const auto messageType = encryptMessageType(); const auto randomLen = olm_encrypt_random_length(m_session); QByteArray randomBuf = getRandom(randomLen); - const auto error = olm_encrypt(m_session, - reinterpret_cast(plaintextBuf.data()), plaintextBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length(), - reinterpret_cast(messageBuf.data()), messageBuf.length()); - - if (error == olm_error()) { - throw lastError(m_session); + if (olm_encrypt(m_session, reinterpret_cast(plaintextBuf.data()), + plaintextBuf.length(), + reinterpret_cast(randomBuf.data()), + randomBuf.length(), + reinterpret_cast(messageBuf.data()), + messageBuf.length()) + == olm_error()) { + throw lastError(); } return QOlmMessage(messageBuf, messageType); @@ -163,9 +170,8 @@ QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, reinterpret_cast(messageBuf.data()), messageBuf.length()); - if (plaintextMaxLen == olm_error()) { - return lastError(m_session); + return lastError(); } QByteArray plaintextBuf(plaintextMaxLen, '0'); @@ -175,10 +181,9 @@ QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, reinterpret_cast(messageBuf2.data()), messageBuf2.length(), reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); - if (plaintextResultLen == olm_error()) { - const auto lastErr = lastError(m_session); - if (lastErr == QOlmError::OutputBufferTooSmall) { + const auto lastErr = lastErrorCode(); + if (lastErr == OLM_OUTPUT_BUFFER_TOO_SMALL) { throw lastErr; } return lastErr; @@ -193,7 +198,7 @@ QOlmMessage::Type QOlmSession::encryptMessageType() { const auto messageTypeResult = olm_encrypt_message_type(m_session); if (messageTypeResult == olm_error()) { - throw lastError(m_session); + throw lastError(); } if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { return QOlmMessage::PreKey; @@ -205,10 +210,10 @@ QByteArray QOlmSession::sessionId() const { const auto idMaxLength = olm_session_id_length(m_session); QByteArray idBuffer(idMaxLength, '0'); - const auto error = olm_session_id(m_session, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - if (error == olm_error()) { - throw lastError(m_session); + if (olm_session_id(m_session, reinterpret_cast(idBuffer.data()), + idBuffer.length()) + == olm_error()) { + throw lastError(); } return idBuffer; } @@ -225,11 +230,10 @@ bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const const auto maybeMatches = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); - - if (maybeMatches == olm_error()) { - return lastError(m_session); - } - return maybeMatches == 1; + if (maybeMatches == olm_error()) + qWarning(E2EE) << "Error matching an inbound session:" + << olm_session_last_error(m_session); + return maybeMatches == 1; // Any errors are treated as non-match } bool QOlmSession::matchesInboundSessionFrom( diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index 021092c7..cc988a03 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -6,7 +6,6 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" -#include "e2ee/qolmerrors.h" #include "e2ee/qolmaccount.h" struct OlmSession; @@ -71,6 +70,9 @@ public: return *lhs < *rhs; } + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + OlmSession* raw() const { return m_session; } QOlmSession(OlmSession* session); diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 84559085..15c875c0 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -8,9 +8,13 @@ using namespace Quotient; -// Convert olm error to enum -QOlmError lastError(OlmUtility *utility) { - return fromString(olm_utility_last_error(utility)); +OlmErrorCode QOlmUtility::lastErrorCode() const { + return olm_utility_last_error_code(m_utility); +} + +const char* QOlmUtility::lastError() const +{ + return olm_utility_last_error(m_utility); } QOlmUtility::QOlmUtility() @@ -48,15 +52,15 @@ QOlmExpected QOlmUtility::ed25519Verify(const QByteArray& key, std::copy(signature.begin(), signature.end(), signatureBuf.begin()); const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), - message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size()); - + message.data(), message.size(), + (void*)signatureBuf.data(), + signatureBuf.size()); if (ret == olm_error()) { - auto error = lastError(m_utility); - if (error == QOlmError::BadMessageMac) { + auto error = lastErrorCode(); + if (error == OLM_BAD_MESSAGE_MAC) return false; - } return error; } - return !ret; // ret == 0 means success + return ret == 0; } diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index 5f6bcdc5..89277385 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -32,8 +32,10 @@ public: QOlmExpected ed25519Verify(const QByteArray &key, const QByteArray &message, const QByteArray &signature); + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + private: OlmUtility *m_utility; - }; } diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 94b44901..114addb5 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "encryptedevent.h" +#include "e2ee/e2ee.h" #include "logging.h" using namespace Quotient; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 02d4c7aa..e24e5745 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -3,10 +3,15 @@ #pragma once -#include "e2ee/e2ee.h" #include "roomevent.h" namespace Quotient { + +constexpr auto CiphertextKeyL = "ciphertext"_ls; +constexpr auto SenderKeyKeyL = "sender_key"_ls; +constexpr auto DeviceIdKeyL = "device_id"_ls; +constexpr auto SessionIdKeyL = "session_id"_ls; + /* * While the specification states: * 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" diff --git a/lib/util.h b/lib/util.h index 9efda5d1..ab219488 100644 --- a/lib/util.h +++ b/lib/util.h @@ -111,6 +111,23 @@ private: iterator to; }; +template +class asKeyValueRange +{ +public: + asKeyValueRange(T& data) + : m_data { data } + {} + + auto begin() { return m_data.keyValueBegin(); } + auto end() { return m_data.keyValueEnd(); } + +private: + T &m_data; +}; +template +asKeyValueRange(T&) -> asKeyValueRange; + /** A replica of std::find_first_of that returns a pair of iterators * * Convenient for cases when you need to know which particular "first of" -- cgit v1.2.3 From bcc05aa1d52cae2b6d8e70bb6cf04fa49904687a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 21 Sep 2022 15:45:59 +0200 Subject: Cleanup across E2EE code Notably: - simplified unnecessarily verbose constructs; - formally aligned (no re-numeration was necessary) QOlmMessage::Type with corresponding OLM_ constants; - dropped QOlmSession::encryptMessageType() because it's very sensitive to the order of calling with QOlmSession::encrypt() (and encrypt() itself already calls it and returns the message type); - simplify the return type of pickle() calls that can only fail due to an internal error; - replace const QString& with QStringView or const QByteArray& where appropriate; - use '\0' where it was meant to be instead of '0'. --- autotests/testgroupsession.cpp | 9 ++-- autotests/testolmaccount.cpp | 3 +- autotests/testolmsession.cpp | 10 +++-- autotests/testolmutility.cpp | 15 +++---- lib/connection.cpp | 8 ++-- lib/e2ee/e2ee.h | 2 +- lib/e2ee/qolmaccount.cpp | 54 +++++++++++----------- lib/e2ee/qolmaccount.h | 9 ++-- lib/e2ee/qolminboundsession.cpp | 57 +++++++++++------------- lib/e2ee/qolminboundsession.h | 4 +- lib/e2ee/qolmmessage.cpp | 6 --- lib/e2ee/qolmmessage.h | 12 +++-- lib/e2ee/qolmoutboundsession.cpp | 44 +++++++++--------- lib/e2ee/qolmoutboundsession.h | 4 +- lib/e2ee/qolmsession.cpp | 96 +++++++++++++++------------------------- lib/e2ee/qolmsession.h | 15 +++---- lib/e2ee/qolmutility.cpp | 24 +++++----- lib/e2ee/qolmutils.h | 1 + lib/events/filesourceinfo.cpp | 35 ++++++--------- lib/keyverificationsession.cpp | 6 +-- 20 files changed, 186 insertions(+), 228 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 3c329a8a..18ace73b 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -17,7 +17,9 @@ void TestGroupSession::groupSessionPicklingValid() QCOMPARE(0, ogs->sessionMessageIndex()); auto ogsPickled = ogs->pickle(Unencrypted {}).value(); - auto ogs2 = QOlmOutboundGroupSession::unpickle(ogsPickled, Unencrypted {}).value(); + auto ogs2 = + QOlmOutboundGroupSession::unpickle(std::move(ogsPickled), Unencrypted{}) + .value(); QCOMPARE(ogsId, ogs2->sessionId()); auto igs = QOlmInboundGroupSession::create(ogs->sessionKey().value()); @@ -29,7 +31,8 @@ void TestGroupSession::groupSessionPicklingValid() QCOMPARE(0, igs->firstKnownIndex()); auto igsPickled = igs->pickle(Unencrypted {}); - igs = QOlmInboundGroupSession::unpickle(igsPickled, Unencrypted {}).value(); + igs = QOlmInboundGroupSession::unpickle(std::move(igsPickled), Unencrypted{}) + .value(); QCOMPARE(igsId, igs->sessionId()); } @@ -39,7 +42,7 @@ void TestGroupSession::groupSessionCryptoValid() auto igs = QOlmInboundGroupSession::create(ogs->sessionKey().value()); QCOMPARE(ogs->sessionId(), igs->sessionId()); - const auto plainText = QStringLiteral("Hello world!"); + const auto plainText = "Hello world!"; const auto ciphertext = ogs->encrypt(plainText).value(); // ciphertext valid base64? QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0); diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index eb428661..0e1eab84 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -25,7 +25,8 @@ void TestOlmAccount::pickleUnpickledTest() auto identityKeys = olmAccount.identityKeys(); auto pickled = olmAccount.pickle(Unencrypted{}).value(); QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); - auto unpickleResult = olmAccount2.unpickle(pickled, Unencrypted{}); + auto unpickleResult = olmAccount2.unpickle(std::move(pickled), + Unencrypted{}); QCOMPARE(unpickleResult, 0); auto identityKeys2 = olmAccount2.identityKeys(); QCOMPARE(identityKeys.curve25519, identityKeys2.curve25519); diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp index 66a04241..18b0d5f2 100644 --- a/autotests/testolmsession.cpp +++ b/autotests/testolmsession.cpp @@ -11,12 +11,14 @@ std::pair createSessionPair() { QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr"); - auto accountA = QOlmAccount("accountA:foo.com", "Device1UserA"); - if (accountA.unpickle(pickledAccountA, Unencrypted{}) != OLM_SUCCESS) + auto accountA = QOlmAccount(u"accountA:foo.com", u"Device1UserA"); + if (accountA.unpickle(std::move(pickledAccountA), Unencrypted{}) + != OLM_SUCCESS) qFatal("Failed to unpickle account A: %s", accountA.lastError()); - auto accountB = QOlmAccount("accountB:foo.com", "Device1UserB"); - if (accountB.unpickle(pickledAccountB, Unencrypted{}) != OLM_SUCCESS) + auto accountB = QOlmAccount(u"accountB:foo.com", u"Device1UserB"); + if (accountB.unpickle(std::move(pickledAccountB), Unencrypted{}) + != OLM_SUCCESS) qFatal("Failed to unpickle account B: %s", accountB.lastError()); diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 5b67c805..64ceb3e7 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -49,7 +49,7 @@ void TestOlmUtility::canonicalJSON() void TestOlmUtility::verifySignedOneTimeKey() { - QOlmAccount aliceOlm { "@alice:matrix.org", "aliceDevice" }; + QOlmAccount aliceOlm { u"@alice:matrix.org", u"aliceDevice" }; aliceOlm.createNewAccount(); aliceOlm.generateOneTimeKeys(1); auto keys = aliceOlm.oneTimeKeys(); @@ -64,16 +64,13 @@ void TestOlmUtility::verifySignedOneTimeKey() auto utility = olm_utility(utilityBuf); - QByteArray signatureBuf1(sig.length(), '0'); + QByteArray signatureBuf1(sig.length(), '\0'); std::copy(sig.begin(), sig.end(), signatureBuf1.begin()); - auto res = olm_ed25519_verify(utility, - aliceOlm.identityKeys().ed25519.data(), - aliceOlm.identityKeys().ed25519.size(), - msg.data(), - msg.size(), - (void *)sig.data(), - sig.size()); + auto res = + olm_ed25519_verify(utility, aliceOlm.identityKeys().ed25519.data(), + aliceOlm.identityKeys().ed25519.size(), msg.data(), + msg.size(), sig.data(), sig.size()); QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); QCOMPARE(res, 0); diff --git a/lib/connection.cpp b/lib/connection.cpp index 865dff79..f38bb751 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -652,8 +652,7 @@ void Connection::Private::completeSetup(const QString& mxId) }); } else { // account already existing - auto pickle = database->accountPickle(); - olmAccount->unpickle(pickle, picklingMode); + olmAccount->unpickle(database->accountPickle(), picklingMode); } #endif // Quotient_E2EE_ENABLED emit q->stateChanged(); @@ -2300,14 +2299,13 @@ std::pair Connection::Private::olmEncryptMessage( { const auto& curveKey = curveKeyForUserDevice(userId, device); const auto& olmSession = olmSessions.at(curveKey).front(); - QOlmMessage::Type type = olmSession->encryptMessageType(); const auto result = olmSession->encrypt(message); if (const auto pickle = olmSession->pickle(picklingMode)) { database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle); } else { qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } - return { type, result.toCiphertext() }; + return { result.type(), result.toCiphertext() }; } bool Connection::Private::createOlmSession(const QString& targetUserId, @@ -2343,7 +2341,7 @@ bool Connection::Private::createOlmSession(const QString& targetUserId, return false; } const auto recipientCurveKey = - curveKeyForUserDevice(targetUserId, targetDeviceId); + curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(); auto session = QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, signedOneTimeKey->key()); diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 51ceff67..5999c0be 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -98,7 +98,7 @@ public: {} //! Unpadded Base64-encoded 32-byte Curve25519 public key - QString key() const { return payload["key"_ls].toString(); } + QByteArray key() const { return payload["key"_ls].toString().toLatin1(); } //! \brief Signatures of the key object //! diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index d33db8e3..556a8274 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -27,11 +27,11 @@ const char* QOlmAccount::lastError() const return olm_account_last_error(m_account); } -QOlmAccount::QOlmAccount(const QString& userId, const QString& deviceId, +QOlmAccount::QOlmAccount(QStringView userId, QStringView deviceId, QObject* parent) : QObject(parent) - , m_userId(userId) - , m_deviceId(deviceId) + , m_userId(userId.toString()) + , m_deviceId(deviceId.toString()) {} QOlmAccount::~QOlmAccount() @@ -43,20 +43,20 @@ QOlmAccount::~QOlmAccount() void QOlmAccount::createNewAccount() { m_account = olm_account(new uint8_t[olm_account_size()]); - size_t randomSize = olm_create_account_random_length(m_account); - QByteArray randomData = getRandom(randomSize); - if (olm_create_account(m_account, randomData.data(), randomSize) + const auto randomLength = olm_create_account_random_length(m_account); + QByteArray randomData = getRandom(randomLength); + if (olm_create_account(m_account, randomData.data(), randomLength) == olm_error()) { throw lastError(); } emit needsSave(); } -OlmErrorCode QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) +OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, const PicklingMode &mode) { m_account = olm_account(new uint8_t[olm_account_size()]); - const QByteArray key = toKey(mode); - if (olm_unpickle_account(m_account, key.data(), key.length(), + if (const auto key = toKey(mode); + olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size()) == olm_error()) { // Probably log the user out since we have no way of getting to the keys @@ -69,7 +69,7 @@ QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) { const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); - QByteArray pickleBuffer(pickleLength, '0'); + QByteArray pickleBuffer(pickleLength, '\0'); if (olm_pickle_account(m_account, key.data(), key.length(), pickleBuffer.data(), pickleLength) == olm_error()) @@ -80,12 +80,12 @@ QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) IdentityKeys QOlmAccount::identityKeys() const { const size_t keyLength = olm_account_identity_keys_length(m_account); - QByteArray keyBuffer(keyLength, '0'); + QByteArray keyBuffer(keyLength, '\0'); if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength) == olm_error()) { throw lastError(); } - const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); + const auto key = QJsonDocument::fromJson(keyBuffer).object(); return IdentityKeys { key.value(QStringLiteral("curve25519")).toString().toUtf8(), key.value(QStringLiteral("ed25519")).toString().toUtf8() @@ -94,7 +94,7 @@ IdentityKeys QOlmAccount::identityKeys() const QByteArray QOlmAccount::sign(const QByteArray &message) const { - QByteArray signatureBuffer(olm_account_signature_length(m_account), '0'); + QByteArray signatureBuffer(olm_account_signature_length(m_account), '\0'); if (olm_account_sign(m_account, message.data(), message.length(), signatureBuffer.data(), signatureBuffer.length()) @@ -112,15 +112,15 @@ QByteArray QOlmAccount::sign(const QJsonObject &message) const QByteArray QOlmAccount::signIdentityKeys() const { const auto keys = identityKeys(); - return sign(QJsonObject { - { "algorithms", QJsonArray { "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" } }, + return sign(QJsonObject{ + { "algorithms", QJsonArray{ "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" } }, { "user_id", m_userId }, { "device_id", m_deviceId }, - { "keys", QJsonObject { { QStringLiteral("curve25519:") + m_deviceId, - QString::fromUtf8(keys.curve25519) }, - { QStringLiteral("ed25519:") + m_deviceId, - QString::fromUtf8(keys.ed25519) } } } }); + { "keys", QJsonObject{ { QStringLiteral("curve25519:") + m_deviceId, + QString::fromUtf8(keys.curve25519) }, + { QStringLiteral("ed25519:") + m_deviceId, + QString::fromUtf8(keys.ed25519) } } } }); } size_t QOlmAccount::maxNumberOfOneTimeKeys() const @@ -130,7 +130,7 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) { - const size_t randomLength = + const auto randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); QByteArray randomBuffer = getRandom(randomLength); @@ -147,8 +147,8 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const { - const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); - QByteArray oneTimeKeysBuffer(static_cast(oneTimeKeyLength), '0'); + const auto oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(static_cast(oneTimeKeyLength), '\0'); if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength) @@ -193,13 +193,13 @@ DeviceKeys QOlmAccount::deviceKeys() const SupportedAlgorithms.cend()); const auto idKeys = identityKeys(); - return DeviceKeys { + return DeviceKeys{ .userId = m_userId, .deviceId = m_deviceId, .algorithms = Algorithms, - .keys { { "curve25519:" + m_deviceId, idKeys.curve25519 }, - { "ed25519:" + m_deviceId, idKeys.ed25519 } }, - .signatures { + .keys{ { "curve25519:" + m_deviceId, idKeys.curve25519 }, + { "ed25519:" + m_deviceId, idKeys.ed25519 } }, + .signatures{ { m_userId, { { "ed25519:" + m_deviceId, signIdentityKeys() } } } } }; } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 5ad98e47..0fb9803f 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -24,7 +24,8 @@ class QUOTIENT_API QOlmAccount : public QObject { Q_OBJECT public: - QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); + QOlmAccount(QStringView userId, QStringView deviceId, + QObject* parent = nullptr); ~QOlmAccount() override; //! Creates a new instance of OlmAccount. During the instantiation @@ -36,7 +37,7 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. //! This needs to be called before any other action or use createNewAccount() instead. - [[nodiscard]] OlmErrorCode unpickle(QByteArray& pickled, + [[nodiscard]] OlmErrorCode unpickle(QByteArray&& pickled, const PicklingMode& mode); //! Serialises an OlmAccount to encrypted Base64. @@ -74,7 +75,7 @@ public: //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! - //! \param message An Olm pre-key message that was encrypted for this account. + //! \param preKeyMessage An Olm pre-key message that was encrypted for this account. QOlmExpected createInboundSession( const QOlmMessage& preKeyMessage); @@ -93,7 +94,7 @@ public: void markKeysAsPublished(); OlmErrorCode lastErrorCode() const; - const char *lastError() const; + const char* lastError() const; // HACK do not use directly QOlmAccount(OlmAccount *account); diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 870070c2..a05ddf62 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "e2ee/qolminboundsession.h" -#include "logging.h" +#include "qolminboundsession.h" +#include "qolmutils.h" +#include "../logging.h" #include #include @@ -37,7 +38,7 @@ std::unique_ptr QOlmInboundGroupSession::create(const Q olmInboundGroupSession, reinterpret_cast(key.constData()), key.size()) == olm_error()) { - // FIXME: create QOlmInboundGroupSession earlier and use lastError() + // FIXME: create QOlmInboundGroupSession earlier and use lastErrorCode() throw olm_inbound_group_session_last_error_code(olmInboundGroupSession); } @@ -47,11 +48,10 @@ std::unique_ptr QOlmInboundGroupSession::create(const Q std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - QByteArray keyBuf = key; if (olm_import_inbound_group_session( olmInboundGroupSession, - reinterpret_cast(keyBuf.data()), keyBuf.size()) + reinterpret_cast(key.data()), key.size()) == olm_error()) { // FIXME: create QOlmInboundGroupSession earlier and use lastError() throw olm_inbound_group_session_last_error_code(olmInboundGroupSession); @@ -60,19 +60,12 @@ std::unique_ptr QOlmInboundGroupSession::import(const Q return std::make_unique(olmInboundGroupSession); } -QByteArray toKey(const PicklingMode &mode) -{ - if (std::holds_alternative(mode)) { - return ""; - } - return std::get(mode).key; -} - -QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode& mode) const { - QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0'); - const QByteArray key = toKey(mode); - if (olm_pickle_inbound_group_session(m_groupSession, key.data(), + QByteArray pickledBuf( + olm_pickle_inbound_group_session_length(m_groupSession), '\0'); + if (const auto key = toKey(mode); + olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), pickledBuf.length()) == olm_error()) { @@ -82,14 +75,13 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const } QOlmExpected QOlmInboundGroupSession::unpickle( - const QByteArray& pickled, const PicklingMode& mode) + QByteArray&& pickled, const PicklingMode& mode) { - QByteArray pickledBuf = pickled; const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); - QByteArray key = toKey(mode); + auto key = toKey(mode); if (olm_unpickle_inbound_group_session(groupSession, key.data(), - key.length(), pickledBuf.data(), - pickledBuf.size()) + key.length(), pickled.data(), + pickled.size()) == olm_error()) { // FIXME: create QOlmInboundGroupSession earlier and use lastError() return olm_inbound_group_session_last_error_code(groupSession); @@ -107,13 +99,16 @@ QOlmExpected> QOlmInboundGroupSession::decrypt( // We need to clone the message because // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(message.length(), '0'); + QByteArray messageBuf(message.length(), '\0'); std::copy(message.begin(), message.end(), messageBuf.begin()); - QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession, - reinterpret_cast(messageBuf.data()), messageBuf.length()), '0'); + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length( + m_groupSession, + reinterpret_cast(messageBuf.data()), + messageBuf.length()), + '\0'); - messageBuf = QByteArray(message.length(), '0'); + messageBuf = QByteArray(message.length(), '\0'); std::copy(message.begin(), message.end(), messageBuf.begin()); const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast(messageBuf.data()), @@ -123,16 +118,17 @@ QOlmExpected> QOlmInboundGroupSession::decrypt( return lastErrorCode(); } - QByteArray output(plaintextLen, '0'); + QByteArray output(plaintextLen, '\0'); std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); return std::make_pair(output, messageIndex); } -QOlmExpected QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +QOlmExpected QOlmInboundGroupSession::exportSession( + uint32_t messageIndex) { const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); - QByteArray keyBuf(keyLength, '0'); + QByteArray keyBuf(keyLength, '\0'); if (olm_export_inbound_group_session( m_groupSession, reinterpret_cast(keyBuf.data()), keyLength, messageIndex) @@ -149,7 +145,8 @@ uint32_t QOlmInboundGroupSession::firstKnownIndex() const QByteArray QOlmInboundGroupSession::sessionId() const { - QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0'); + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), + '\0'); if (olm_inbound_group_session_id( m_groupSession, reinterpret_cast(sessionIdBuf.data()), sessionIdBuf.length()) diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index e8da6355..0b89349a 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -21,11 +21,11 @@ public: //! Import an inbound group session, from a previous export. static std::unique_ptr import(const QByteArray& key); //! Serialises an `OlmInboundGroupSession` to encrypted Base64. - QByteArray pickle(const PicklingMode &mode) const; + QByteArray pickle(const PicklingMode& mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. static QOlmExpected unpickle( - const QByteArray& pickled, const PicklingMode& mode); + QByteArray&& pickled, const PicklingMode& mode); //! Decrypts ciphertext received for this group session. QOlmExpected > decrypt(const QByteArray& message); //! Export the base64-encoded ratchet key for this session, at the given index, diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp index f9b4a5c2..b9cb8bd2 100644 --- a/lib/e2ee/qolmmessage.cpp +++ b/lib/e2ee/qolmmessage.cpp @@ -15,12 +15,6 @@ QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty"); } -QOlmMessage::QOlmMessage(const QOlmMessage &message) - : QByteArray(message) - , m_messageType(message.type()) -{ -} - QOlmMessage::Type QOlmMessage::type() const { return m_messageType; diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h index b4285a93..ea73b3e3 100644 --- a/lib/e2ee/qolmmessage.h +++ b/lib/e2ee/qolmmessage.h @@ -6,8 +6,9 @@ #include "quotient_export.h" -#include -#include +#include +#include +#include namespace Quotient { @@ -22,15 +23,12 @@ class QUOTIENT_API QOlmMessage : public QByteArray { Q_GADGET public: enum Type { - PreKey = 0, - General, + PreKey = OLM_MESSAGE_TYPE_PRE_KEY, + General = OLM_MESSAGE_TYPE_MESSAGE, }; Q_ENUM(Type) - QOlmMessage() = default; explicit QOlmMessage(QByteArray ciphertext, Type type = General); - explicit QOlmMessage(const QOlmMessage &message); - ~QOlmMessage() = default; static QOlmMessage fromCiphertext(const QByteArray &ciphertext); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 79c16e01..22107a21 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -2,9 +2,10 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#include "e2ee/qolmoutboundsession.h" +#include "qolmoutboundsession.h" -#include "e2ee/qolmutils.h" +#include "logging.h" +#include "qolmutils.h" #include @@ -43,8 +44,9 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() throw olm_outbound_group_session_last_error_code(olmOutboundGroupSession); } + // FIXME: is it used anywhere? const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); - QByteArray keyBuffer(keyMaxLength, '0'); + QByteArray keyBuffer(keyMaxLength, '\0'); olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), keyMaxLength); @@ -55,8 +57,9 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const { - QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); - QByteArray key = toKey(mode); + QByteArray pickledBuf( + olm_pickle_outbound_group_session_length(m_groupSession), '\0'); + auto key = toKey(mode); if (olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(), pickledBuf.length()) @@ -64,40 +67,41 @@ QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mo return lastErrorCode(); key.clear(); - return pickledBuf; } -QOlmExpected QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +QOlmExpected QOlmOutboundGroupSession::unpickle( + QByteArray&& pickled, const PicklingMode& mode) { - QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - QByteArray key = toKey(mode); + auto key = toKey(mode); if (olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), - key.length(), pickledBuf.data(), - pickledBuf.length()) + key.length(), pickled.data(), + pickled.length()) == olm_error()) { // FIXME: create the session object earlier and use lastError() return olm_outbound_group_session_last_error_code( olmOutboundGroupSession); } const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); - QByteArray idBuffer(idMaxLength, '0'); + QByteArray idBuffer(idMaxLength, '\0'); olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), idBuffer.length()); + // FIXME: idBuffer doesn't seem to be used, is it needed here? key.clear(); return std::make_unique(olmOutboundGroupSession); } -QOlmExpected QOlmOutboundGroupSession::encrypt(const QString &plaintext) const +QOlmExpected QOlmOutboundGroupSession::encrypt( + const QByteArray& plaintext) const { - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLength, '0'); + const auto messageMaxLength = + olm_group_encrypt_message_length(m_groupSession, plaintext.length()); + QByteArray messageBuf(messageMaxLength, '\0'); if (olm_group_encrypt(m_groupSession, - reinterpret_cast(plaintextBuf.data()), - plaintextBuf.length(), + reinterpret_cast(plaintext.data()), + plaintext.length(), reinterpret_cast(messageBuf.data()), messageBuf.length()) == olm_error()) @@ -114,7 +118,7 @@ uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const QByteArray QOlmOutboundGroupSession::sessionId() const { const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); - QByteArray idBuffer(idMaxLength, '0'); + QByteArray idBuffer(idMaxLength, '\0'); if (olm_outbound_group_session_id( m_groupSession, reinterpret_cast(idBuffer.data()), idBuffer.length()) @@ -127,7 +131,7 @@ QByteArray QOlmOutboundGroupSession::sessionId() const QOlmExpected QOlmOutboundGroupSession::sessionKey() const { const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); - QByteArray keyBuffer(keyMaxLength, '0'); + QByteArray keyBuffer(keyMaxLength, '\0'); if (olm_outbound_group_session_key( m_groupSession, reinterpret_cast(keyBuffer.data()), keyMaxLength) diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index cd26fc67..e5e73ddf 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -24,10 +24,10 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( - const QByteArray& pickled, const PicklingMode& mode); + QByteArray&& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. - QOlmExpected encrypt(const QString& plaintext) const; + QOlmExpected encrypt(const QByteArray& plaintext) const; //! Get the current message index for this session. //! diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 771d310d..e252c37f 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -76,22 +76,17 @@ QOlmExpected QOlmSession::createInboundSessionFrom( } QOlmExpected QOlmSession::createOutboundSession( - QOlmAccount* account, const QString& theirIdentityKey, - const QString& theirOneTimeKey) + QOlmAccount* account, const QByteArray& theirIdentityKey, + const QByteArray& theirOneTimeKey) { - auto *olmOutboundSession = create(); - const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); - QByteArray randomBuf = getRandom(randomLen); + auto* olmOutboundSession = create(); + auto randomBuf = getRandom( + olm_create_outbound_session_random_length(olmOutboundSession)); - QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); - QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8(); if (olm_create_outbound_session( - olmOutboundSession, account->data(), - reinterpret_cast(theirIdentityKeyBuf.data()), - theirIdentityKeyBuf.length(), - reinterpret_cast(theirOneTimeKeyBuf.data()), - theirOneTimeKeyBuf.length(), - reinterpret_cast(randomBuf.data()), randomBuf.length()) + olmOutboundSession, account->data(), theirIdentityKey.data(), + theirIdentityKey.length(), theirOneTimeKey.data(), + theirOneTimeKey.length(), randomBuf.data(), randomBuf.length()) == olm_error()) { // FIXME: the QOlmSession object should be created earlier const auto lastErr = olm_session_last_error_code(olmOutboundSession); @@ -107,7 +102,7 @@ QOlmExpected QOlmSession::createOutboundSession( QOlmExpected QOlmSession::pickle(const PicklingMode &mode) const { - QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); + QByteArray pickledBuf(olm_pickle_session_length(m_session), '\0'); QByteArray key = toKey(mode); if (olm_pickle_session(m_session, key.data(), key.length(), pickledBuf.data(), pickledBuf.length()) @@ -118,14 +113,13 @@ QOlmExpected QOlmSession::pickle(const PicklingMode &mode) const return pickledBuf; } -QOlmExpected QOlmSession::unpickle(const QByteArray& pickled, +QOlmExpected QOlmSession::unpickle(QByteArray&& pickled, const PicklingMode& mode) { - QByteArray pickledBuf = pickled; auto *olmSession = create(); - QByteArray key = toKey(mode); + auto key = toKey(mode); if (olm_unpickle_session(olmSession, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()) + pickled.data(), pickled.length()) == olm_error()) { // FIXME: the QOlmSession object should be created earlier return olm_session_last_error_code(olmSession); @@ -135,52 +129,48 @@ QOlmExpected QOlmSession::unpickle(const QByteArray& pickled, return std::make_unique(olmSession); } -QOlmMessage QOlmSession::encrypt(const QString &plaintext) +QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) { - QByteArray plaintextBuf = plaintext.toUtf8(); - const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length()); - QByteArray messageBuf(messageMaxLen, '0'); - const auto messageType = encryptMessageType(); - const auto randomLen = olm_encrypt_random_length(m_session); - QByteArray randomBuf = getRandom(randomLen); - if (olm_encrypt(m_session, reinterpret_cast(plaintextBuf.data()), - plaintextBuf.length(), - reinterpret_cast(randomBuf.data()), - randomBuf.length(), - reinterpret_cast(messageBuf.data()), + const auto messageMaxLength = + olm_encrypt_message_length(m_session, plaintext.length()); + QByteArray messageBuf(messageMaxLength, '0'); + // NB: The type has to be calculated before calling olm_encrypt() + const auto messageType = olm_encrypt_message_type(m_session); + auto randomBuf = getRandom(olm_encrypt_random_length(m_session)); + if (olm_encrypt(m_session, plaintext.data(), plaintext.length(), + randomBuf.data(), randomBuf.length(), messageBuf.data(), messageBuf.length()) == olm_error()) { throw lastError(); } - return QOlmMessage(messageBuf, messageType); + randomBuf.clear(); + return QOlmMessage(messageBuf, QOlmMessage::Type(messageType)); } QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const { - const auto messageType = message.type(); const auto ciphertext = message.toCiphertext(); - const auto messageTypeValue = messageType == QOlmMessage::Type::General - ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY; + const auto messageTypeValue = message.type(); // We need to clone the message because // olm_decrypt_max_plaintext_length destroys the input buffer - QByteArray messageBuf(ciphertext.length(), '0'); + QByteArray messageBuf(ciphertext.length(), '\0'); std::copy(message.begin(), message.end(), messageBuf.begin()); - const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue, - reinterpret_cast(messageBuf.data()), messageBuf.length()); + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length( + m_session, messageTypeValue, messageBuf.data(), messageBuf.length()); if (plaintextMaxLen == olm_error()) { return lastError(); } - QByteArray plaintextBuf(plaintextMaxLen, '0'); - QByteArray messageBuf2(ciphertext.length(), '0'); + QByteArray plaintextBuf(plaintextMaxLen, '\0'); + QByteArray messageBuf2(ciphertext.length(), '\0'); std::copy(message.begin(), message.end(), messageBuf2.begin()); - const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue, - reinterpret_cast(messageBuf2.data()), messageBuf2.length(), - reinterpret_cast(plaintextBuf.data()), plaintextMaxLen); + const auto plaintextResultLen = + olm_decrypt(m_session, messageTypeValue, messageBuf2.data(), + messageBuf2.length(), plaintextBuf.data(), plaintextMaxLen); if (plaintextResultLen == olm_error()) { const auto lastErr = lastErrorCode(); if (lastErr == OLM_OUTPUT_BUFFER_TOO_SMALL) { @@ -188,31 +178,15 @@ QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const } return lastErr; } - QByteArray output(plaintextResultLen, '0'); - std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen); - plaintextBuf.clear(); - return output; -} - -QOlmMessage::Type QOlmSession::encryptMessageType() -{ - const auto messageTypeResult = olm_encrypt_message_type(m_session); - if (messageTypeResult == olm_error()) { - throw lastError(); - } - if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) { - return QOlmMessage::PreKey; - } - return QOlmMessage::General; + plaintextBuf.truncate(plaintextResultLen); + return plaintextBuf; } QByteArray QOlmSession::sessionId() const { const auto idMaxLength = olm_session_id_length(m_session); QByteArray idBuffer(idMaxLength, '0'); - if (olm_session_id(m_session, reinterpret_cast(idBuffer.data()), - idBuffer.length()) - == olm_error()) { + if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error()) { throw lastError(); } return idBuffer; diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index cc988a03..174e98ef 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -26,18 +26,18 @@ public: const QOlmMessage& preKeyMessage); static QOlmExpected createOutboundSession( - QOlmAccount* account, const QString& theirIdentityKey, - const QString& theirOneTimeKey); + QOlmAccount* account, const QByteArray& theirIdentityKey, + const QByteArray& theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. QOlmExpected pickle(const PicklingMode &mode) const; - //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static QOlmExpected unpickle( - const QByteArray& pickled, const PicklingMode& mode); + //! Deserialises from encrypted Base64 previously made with pickle() + static QOlmExpected unpickle(QByteArray&& pickled, + const PicklingMode& mode); //! Encrypts a plaintext message using the session. - QOlmMessage encrypt(const QString &plaintext); + QOlmMessage encrypt(const QByteArray& plaintext); //! Decrypts a message using this session. Decoding is lossy, meaning if //! the decrypted plaintext contains invalid UTF-8 symbols, they will @@ -47,9 +47,6 @@ public: //! Get a base64-encoded identifier for this session. QByteArray sessionId() const; - //! The type of the next message that will be returned from encryption. - QOlmMessage::Type encryptMessageType(); - //! Checker for any received messages for this session. bool hasReceivedMessage() const; diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 15c875c0..08c2b699 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -32,7 +32,7 @@ QOlmUtility::~QOlmUtility() QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const { const auto outputLen = olm_sha256_length(m_utility); - QByteArray outputBuf(outputLen, '0'); + QByteArray outputBuf(outputLen, '\0'); olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), outputBuf.data(), outputBuf.length()); @@ -48,19 +48,17 @@ QOlmExpected QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message, const QByteArray& signature) { - QByteArray signatureBuf(signature.length(), '0'); + QByteArray signatureBuf(signature.length(), '\0'); std::copy(signature.begin(), signature.end(), signatureBuf.begin()); - const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(), - message.data(), message.size(), - (void*)signatureBuf.data(), - signatureBuf.size()); - if (ret == olm_error()) { - auto error = lastErrorCode(); - if (error == OLM_BAD_MESSAGE_MAC) - return false; - return error; - } + if (olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), + message.size(), signatureBuf.data(), + signatureBuf.size()) + == 0) + return true; - return ret == 0; + auto error = lastErrorCode(); + if (error == OLM_BAD_MESSAGE_MAC) + return false; + return error; } diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h index f218e628..7a8511c3 100644 --- a/lib/e2ee/qolmutils.h +++ b/lib/e2ee/qolmutils.h @@ -9,6 +9,7 @@ #include "e2ee/e2ee.h" namespace Quotient { + // Convert PicklingMode to key QUOTIENT_API QByteArray toKey(const PicklingMode &mode); QUOTIENT_API QByteArray getRandom(size_t bufferSize); diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index e8b6794b..6abe6a08 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -59,19 +59,15 @@ std::pair Quotient::encryptFile( const QByteArray& plainText) { #ifdef Quotient_E2EE_ENABLED - QByteArray k = getRandom(32); - auto kBase64 = k.toBase64(); - QByteArray iv = getRandom(16); - JWK key = { "oct"_ls, - { "encrypt"_ls, "decrypt"_ls }, - "A256CTR"_ls, - QString(k.toBase64()) - .replace(u'/', u'_') - .replace(u'+', u'-') - .left(kBase64.indexOf('=')), - true }; - - int length; + auto k = getRandom(32); + auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + auto iv = getRandom(16); + JWK key = { + "oct"_ls, { "encrypt"_ls, "decrypt"_ls }, "A256CTR"_ls, kBase64, true + }; + + int length = -1; auto* ctx = EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()), @@ -89,14 +85,11 @@ std::pair Quotient::encryptFile( EVP_CIPHER_CTX_free(ctx); auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256) - .toBase64(); - auto ivBase64 = iv.toBase64(); - EncryptedFileMetadata efm = { {}, - key, - ivBase64.left(ivBase64.indexOf('=')), - { { QStringLiteral("sha256"), - hash.left(hash.indexOf('=')) } }, - "v2"_ls }; + .toBase64(QByteArray::OmitTrailingEquals); + auto ivBase64 = iv.toBase64(QByteArray::OmitTrailingEquals); + EncryptedFileMetadata efm = { + {}, key, ivBase64, { { QStringLiteral("sha256"), hash } }, "v2"_ls + }; return { efm, cipherText }; #else return {}; diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 3f76eac1..0f24c743 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -71,9 +71,9 @@ void KeyVerificationSession::init(milliseconds timeout) QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); m_sas = olm_sas(new std::byte[olm_sas_size()]); - auto randomSize = olm_create_sas_random_length(m_sas); - auto random = getRandom(randomSize); - olm_create_sas(m_sas, random.data(), randomSize); + const auto randomLength = olm_create_sas_random_length(m_sas); + auto random = getRandom(randomLength); + olm_create_sas(m_sas, random.data(), randomLength); } KeyVerificationSession::~KeyVerificationSession() -- cgit v1.2.3 From bc1ded73bedf593acda80b00eb7da32f688c4843 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Wed, 21 Sep 2022 16:11:39 +0200 Subject: RandomBuffer A convenient abstraction swallowing all the type casts and, more importantly, cleanup on destruction (previous code only cleaned up the buffer upon a successful call to Olm API but not upon an error). --- lib/connection.cpp | 2 +- lib/e2ee/qolmaccount.cpp | 9 +++------ lib/e2ee/qolmoutboundsession.cpp | 9 +++------ lib/e2ee/qolmsession.cpp | 12 +++++------- lib/e2ee/qolmutils.cpp | 7 +++---- lib/e2ee/qolmutils.h | 23 ++++++++++++++++++++++- lib/events/filesourceinfo.cpp | 8 +++----- lib/keyverificationsession.cpp | 3 +-- 8 files changed, 41 insertions(+), 32 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index f38bb751..cd8ee727 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -614,7 +614,7 @@ void Connection::Private::completeSetup(const QString& mxId) loop.exec(); if (job.error() == QKeychain::Error::EntryNotFound) { - picklingMode = Encrypted { getRandom(128) }; + picklingMode = Encrypted { RandomBuffer(128) }; QKeychain::WritePasswordJob job(qAppName()); job.setAutoDelete(false); job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 556a8274..b56272ef 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -44,8 +44,7 @@ void QOlmAccount::createNewAccount() { m_account = olm_account(new uint8_t[olm_account_size()]); const auto randomLength = olm_create_account_random_length(m_account); - QByteArray randomData = getRandom(randomLength); - if (olm_create_account(m_account, randomData.data(), randomLength) + if (olm_create_account(m_account, RandomBuffer(randomLength), randomLength) == olm_error()) { throw lastError(); } @@ -133,10 +132,8 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const auto randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); - QByteArray randomBuffer = getRandom(randomLength); - const auto result = - olm_account_generate_one_time_keys(m_account, numberOfKeys, - randomBuffer.data(), randomLength); + const auto result = olm_account_generate_one_time_keys( + m_account, numberOfKeys, RandomBuffer(randomLength), randomLength); if (result == olm_error()) { throw lastError(); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 22107a21..3d176274 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -34,11 +34,10 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() { auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); - QByteArray randomBuf = getRandom(randomLength); - if (olm_init_outbound_group_session( - olmOutboundGroupSession, - reinterpret_cast(randomBuf.data()), randomBuf.length()) + if (olm_init_outbound_group_session(olmOutboundGroupSession, + RandomBuffer(randomLength).bytes(), + randomLength) == olm_error()) { // FIXME: create the session object earlier and use lastError() throw olm_outbound_group_session_last_error_code(olmOutboundGroupSession); @@ -50,8 +49,6 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), keyMaxLength); - randomBuf.clear(); - return std::make_unique(olmOutboundGroupSession); } diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index e252c37f..7c102a96 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -80,13 +80,13 @@ QOlmExpected QOlmSession::createOutboundSession( const QByteArray& theirOneTimeKey) { auto* olmOutboundSession = create(); - auto randomBuf = getRandom( - olm_create_outbound_session_random_length(olmOutboundSession)); + const auto randomLength = + olm_create_outbound_session_random_length(olmOutboundSession); if (olm_create_outbound_session( olmOutboundSession, account->data(), theirIdentityKey.data(), theirIdentityKey.length(), theirOneTimeKey.data(), - theirOneTimeKey.length(), randomBuf.data(), randomBuf.length()) + theirOneTimeKey.length(), RandomBuffer(randomLength), randomLength) == olm_error()) { // FIXME: the QOlmSession object should be created earlier const auto lastErr = olm_session_last_error_code(olmOutboundSession); @@ -96,7 +96,6 @@ QOlmExpected QOlmSession::createOutboundSession( return lastErr; } - randomBuf.clear(); return std::make_unique(olmOutboundSession); } @@ -136,15 +135,14 @@ QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) QByteArray messageBuf(messageMaxLength, '0'); // NB: The type has to be calculated before calling olm_encrypt() const auto messageType = olm_encrypt_message_type(m_session); - auto randomBuf = getRandom(olm_encrypt_random_length(m_session)); + const auto randomLength = olm_encrypt_random_length(m_session); if (olm_encrypt(m_session, plaintext.data(), plaintext.length(), - randomBuf.data(), randomBuf.length(), messageBuf.data(), + RandomBuffer(randomLength), randomLength, messageBuf.data(), messageBuf.length()) == olm_error()) { throw lastError(); } - randomBuf.clear(); return QOlmMessage(messageBuf, QOlmMessage::Type(messageType)); } diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp index 6f7937e8..c6e51bcd 100644 --- a/lib/e2ee/qolmutils.cpp +++ b/lib/e2ee/qolmutils.cpp @@ -15,9 +15,8 @@ QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) return std::get(mode).key; } -QByteArray Quotient::getRandom(size_t bufferSize) +RandomBuffer::RandomBuffer(size_t size) + : QByteArray(static_cast(size), '\0') { - QByteArray buffer(bufferSize, '0'); - QRandomGenerator::system()->generate(buffer.begin(), buffer.end()); - return buffer; + QRandomGenerator::system()->generate(begin(), end()); } diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h index 7a8511c3..da9d2d18 100644 --- a/lib/e2ee/qolmutils.h +++ b/lib/e2ee/qolmutils.h @@ -12,5 +12,26 @@ namespace Quotient { // Convert PicklingMode to key QUOTIENT_API QByteArray toKey(const PicklingMode &mode); -QUOTIENT_API QByteArray getRandom(size_t bufferSize); + +class QUOTIENT_API RandomBuffer : public QByteArray { +public: + explicit RandomBuffer(size_t size); + ~RandomBuffer() { clear(); } + + // NOLINTNEXTLINE(google-explicit-constructor) + QUO_IMPLICIT operator void*() { return data(); } + char* chars() { return data(); } + uint8_t* bytes() { return reinterpret_cast(data()); } + + Q_DISABLE_COPY(RandomBuffer) + RandomBuffer(RandomBuffer&&) = default; + void operator=(RandomBuffer&&) = delete; +}; + +[[deprecated("Create RandomBuffer directly")]] inline auto getRandom( + size_t bufferSize) +{ + return RandomBuffer(bufferSize); } + +} // namespace Quotient diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp index 6abe6a08..a60d86d2 100644 --- a/lib/events/filesourceinfo.cpp +++ b/lib/events/filesourceinfo.cpp @@ -59,19 +59,17 @@ std::pair Quotient::encryptFile( const QByteArray& plainText) { #ifdef Quotient_E2EE_ENABLED - auto k = getRandom(32); + auto k = RandomBuffer(32); auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - auto iv = getRandom(16); + auto iv = RandomBuffer(16); JWK key = { "oct"_ls, { "encrypt"_ls, "decrypt"_ls }, "A256CTR"_ls, kBase64, true }; int length = -1; auto* ctx = EVP_CIPHER_CTX_new(); - EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, - reinterpret_cast(k.data()), - reinterpret_cast(iv.data())); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, k.bytes(), iv.bytes()); const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 0f24c743..171596c0 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -72,8 +72,7 @@ void KeyVerificationSession::init(milliseconds timeout) m_sas = olm_sas(new std::byte[olm_sas_size()]); const auto randomLength = olm_create_sas_random_length(m_sas); - auto random = getRandom(randomLength); - olm_create_sas(m_sas, random.data(), randomLength); + olm_create_sas(m_sas, RandomBuffer(randomLength), randomLength); } KeyVerificationSession::~KeyVerificationSession() -- 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] --- autotests/testgroupsession.cpp | 8 +++---- autotests/testolmaccount.cpp | 2 +- lib/connection.cpp | 23 ++++++-------------- lib/database.cpp | 32 +++++++++++++--------------- lib/e2ee/qolmaccount.cpp | 37 +++++++++++++++++++------------- lib/e2ee/qolmaccount.h | 4 +++- lib/e2ee/qolminboundsession.cpp | 28 +++++++++++++++++------- lib/e2ee/qolminboundsession.h | 4 ++-- lib/e2ee/qolmoutboundsession.cpp | 24 ++++++++++++--------- lib/e2ee/qolmoutboundsession.h | 6 +++--- lib/e2ee/qolmsession.cpp | 46 ++++++++++++++++++++++------------------ lib/e2ee/qolmsession.h | 2 +- lib/e2ee/qolmutils.h | 18 ++++++++++++++++ lib/room.cpp | 45 ++++++++++++++------------------------- 14 files changed, 150 insertions(+), 129 deletions(-) diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp index 18ace73b..1054a160 100644 --- a/autotests/testgroupsession.cpp +++ b/autotests/testgroupsession.cpp @@ -16,13 +16,13 @@ void TestGroupSession::groupSessionPicklingValid() QVERIFY(QByteArray::fromBase64(ogsId).size() > 0); QCOMPARE(0, ogs->sessionMessageIndex()); - auto ogsPickled = ogs->pickle(Unencrypted {}).value(); + auto&& ogsPickled = ogs->pickle(Unencrypted {}); auto ogs2 = QOlmOutboundGroupSession::unpickle(std::move(ogsPickled), Unencrypted{}) .value(); QCOMPARE(ogsId, ogs2->sessionId()); - auto igs = QOlmInboundGroupSession::create(ogs->sessionKey().value()); + auto igs = QOlmInboundGroupSession::create(ogs->sessionKey()).value(); const auto igsId = igs->sessionId(); // ID is valid base64? QVERIFY(QByteArray::fromBase64(igsId).size() > 0); @@ -39,11 +39,11 @@ void TestGroupSession::groupSessionPicklingValid() void TestGroupSession::groupSessionCryptoValid() { auto ogs = QOlmOutboundGroupSession::create(); - auto igs = QOlmInboundGroupSession::create(ogs->sessionKey().value()); + auto igs = QOlmInboundGroupSession::create(ogs->sessionKey()).value(); QCOMPARE(ogs->sessionId(), igs->sessionId()); const auto plainText = "Hello world!"; - const auto ciphertext = ogs->encrypt(plainText).value(); + const auto ciphertext = ogs->encrypt(plainText); // ciphertext valid base64? QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0); diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 0e1eab84..56532698 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -23,7 +23,7 @@ void TestOlmAccount::pickleUnpickledTest() QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); olmAccount.createNewAccount(); auto identityKeys = olmAccount.identityKeys(); - auto pickled = olmAccount.pickle(Unencrypted{}).value(); + auto pickled = olmAccount.pickle(Unencrypted{}); QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); auto unpickleResult = olmAccount2.unpickle(std::move(pickled), Unencrypted{}); diff --git a/lib/connection.cpp b/lib/connection.cpp index cd8ee727..0afbffbc 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -206,13 +206,9 @@ public: } void saveSession(const QOlmSession& session, const QString& senderKey) const { - if (auto pickleResult = session.pickle(picklingMode)) - q->database()->saveOlmSession(senderKey, session.sessionId(), - *pickleResult, - QDateTime::currentDateTime()); - else - qCWarning(E2EE) << "Failed to pickle olm session. Error" - << pickleResult.error(); + q->database()->saveOlmSession(senderKey, session.sessionId(), + session.pickle(picklingMode), + QDateTime::currentDateTime()); } template @@ -2219,11 +2215,7 @@ void Connection::saveOlmAccount() { #ifdef Quotient_E2EE_ENABLED qCDebug(E2EE) << "Saving olm account"; - if (const auto expectedPickle = d->olmAccount->pickle(d->picklingMode)) - d->database->setAccountPickle(*expectedPickle); - else - qCWarning(E2EE) << "Couldn't save Olm account pickle:" - << expectedPickle.error(); + d->database->setAccountPickle(d->olmAccount->pickle(d->picklingMode)); #endif } @@ -2300,11 +2292,8 @@ std::pair Connection::Private::olmEncryptMessage( const auto& curveKey = curveKeyForUserDevice(userId, device); const auto& olmSession = olmSessions.at(curveKey).front(); const auto result = olmSession->encrypt(message); - if (const auto pickle = olmSession->pickle(picklingMode)) { - database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle); - } else { - qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); - } + database->updateOlmSession(curveKey, olmSession->sessionId(), + olmSession->pickle(picklingMode)); return { result.type(), result.toCiphertext() }; } diff --git a/lib/database.cpp b/lib/database.cpp index 4eb82cf5..2b472648 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -323,23 +323,21 @@ void Database::saveCurrentOutboundMegolmSession( const QOlmOutboundGroupSession& session) { const auto pickle = session.pickle(picklingMode); - if (pickle) { - auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); - deleteQuery.bindValue(":roomId", roomId); - deleteQuery.bindValue(":sessionId", session.sessionId()); - - auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); - insertQuery.bindValue(":roomId", roomId); - insertQuery.bindValue(":sessionId", session.sessionId()); - insertQuery.bindValue(":pickle", pickle.value()); - insertQuery.bindValue(":creationTime", session.creationTime()); - insertQuery.bindValue(":messageCount", session.messageCount()); - - transaction(); - execute(deleteQuery); - execute(insertQuery); - commit(); - } + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); + deleteQuery.bindValue(":roomId", roomId); + deleteQuery.bindValue(":sessionId", session.sessionId()); + + auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); + insertQuery.bindValue(":roomId", roomId); + insertQuery.bindValue(":sessionId", session.sessionId()); + insertQuery.bindValue(":pickle", pickle); + insertQuery.bindValue(":creationTime", session.creationTime()); + insertQuery.bindValue(":messageCount", session.messageCount()); + + transaction(); + execute(deleteQuery); + execute(insertQuery); + commit(); } QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index b56272ef..d03bcb3b 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -45,9 +45,9 @@ void QOlmAccount::createNewAccount() m_account = olm_account(new uint8_t[olm_account_size()]); const auto randomLength = olm_create_account_random_length(m_account); if (olm_create_account(m_account, RandomBuffer(randomLength), randomLength) - == olm_error()) { - throw lastError(); - } + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to create a new account"); + emit needsSave(); } @@ -64,7 +64,7 @@ OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, const PicklingMode &mod return OLM_SUCCESS; } -QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) +QByteArray QOlmAccount::pickle(const PicklingMode &mode) { const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); @@ -72,7 +72,9 @@ QOlmExpected QOlmAccount::pickle(const PicklingMode &mode) if (olm_pickle_account(m_account, key.data(), key.length(), pickleBuffer.data(), pickleLength) == olm_error()) - return lastErrorCode(); + QOLM_INTERNAL_ERROR(qPrintable("Failed to pickle Olm account " + + accountId())); + return pickleBuffer; } @@ -82,7 +84,8 @@ IdentityKeys QOlmAccount::identityKeys() const QByteArray keyBuffer(keyLength, '\0'); if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength) == olm_error()) { - throw lastError(); + QOLM_INTERNAL_ERROR( + qPrintable("Failed to get " % accountId() % " identity keys")); } const auto key = QJsonDocument::fromJson(keyBuffer).object(); return IdentityKeys { @@ -97,9 +100,9 @@ QByteArray QOlmAccount::sign(const QByteArray &message) const if (olm_account_sign(m_account, message.data(), message.length(), signatureBuffer.data(), signatureBuffer.length()) - == olm_error()) { - throw lastError(); - } + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to sign a message"); + return signatureBuffer; } @@ -135,9 +138,10 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const auto result = olm_account_generate_one_time_keys( m_account, numberOfKeys, RandomBuffer(randomLength), randomLength); - if (result == olm_error()) { - throw lastError(); - } + if (result == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable( + "Failed to generate one-time keys for account " + accountId())); + emit needsSave(); return result; } @@ -149,9 +153,10 @@ UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength) - == olm_error()) { - throw lastError(); - } + == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable( + "Failed to obtain one-time keys for account" % accountId())); + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); UnsignedOneTimeKeys oneTimeKeys; fromJson(json, oneTimeKeys.keys); @@ -266,3 +271,5 @@ bool Quotient::ed25519VerifySignature(const QString& signingKey, return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf) .value_or(false); } + +QString QOlmAccount::accountId() const { return m_userId % '/' % m_deviceId; } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 0fb9803f..a5faa82a 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -41,7 +41,7 @@ public: const PicklingMode& mode); //! Serialises an OlmAccount to encrypted Base64. - QOlmExpected pickle(const PicklingMode &mode); + QByteArray pickle(const PicklingMode &mode); //! Returns the account's public identity keys already formatted as JSON IdentityKeys identityKeys() const; @@ -107,6 +107,8 @@ private: OlmAccount *m_account = nullptr; // owning QString m_userId; QString m_deviceId; + + QString accountId() const; }; QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys, diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index a05ddf62..1e3803f2 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -31,7 +31,8 @@ QOlmInboundGroupSession::~QOlmInboundGroupSession() //delete[](reinterpret_cast(m_groupSession)); } -std::unique_ptr QOlmInboundGroupSession::create(const QByteArray &key) +QOlmExpected QOlmInboundGroupSession::create( + const QByteArray& key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); if (olm_init_inbound_group_session( @@ -39,13 +40,17 @@ std::unique_ptr QOlmInboundGroupSession::create(const Q reinterpret_cast(key.constData()), key.size()) == olm_error()) { // FIXME: create QOlmInboundGroupSession earlier and use lastErrorCode() - throw olm_inbound_group_session_last_error_code(olmInboundGroupSession); + qWarning(E2EE) << "Failed to create an inbound group session:" + << olm_inbound_group_session_last_error( + olmInboundGroupSession); + return olm_inbound_group_session_last_error_code(olmInboundGroupSession); } return std::make_unique(olmInboundGroupSession); } -std::unique_ptr QOlmInboundGroupSession::import(const QByteArray &key) +QOlmExpected QOlmInboundGroupSession::import( + const QByteArray& key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); @@ -54,7 +59,10 @@ std::unique_ptr QOlmInboundGroupSession::import(const Q reinterpret_cast(key.data()), key.size()) == olm_error()) { // FIXME: create QOlmInboundGroupSession earlier and use lastError() - throw olm_inbound_group_session_last_error_code(olmInboundGroupSession); + qWarning(E2EE) << "Failed to import an inbound group session:" + << olm_inbound_group_session_last_error( + olmInboundGroupSession); + return olm_inbound_group_session_last_error_code(olmInboundGroupSession); } return std::make_unique(olmInboundGroupSession); @@ -69,7 +77,7 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode& mode) const key.length(), pickledBuf.data(), pickledBuf.length()) == olm_error()) { - throw lastError(); + QOLM_INTERNAL_ERROR("Failed to pickle the inbound group session"); } return pickledBuf; } @@ -84,6 +92,8 @@ QOlmExpected QOlmInboundGroupSession::unpickle( pickled.size()) == olm_error()) { // FIXME: create QOlmInboundGroupSession earlier and use lastError() + qWarning(E2EE) << "Failed to unpickle an inbound group session:" + << olm_inbound_group_session_last_error(groupSession); return olm_inbound_group_session_last_error_code(groupSession); } key.clear(); @@ -133,6 +143,8 @@ QOlmExpected QOlmInboundGroupSession::exportSession( m_groupSession, reinterpret_cast(keyBuf.data()), keyLength, messageIndex) == olm_error()) { + QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to export the inbound group session"); return lastErrorCode(); } return keyBuf; @@ -150,9 +162,9 @@ QByteArray QOlmInboundGroupSession::sessionId() const if (olm_inbound_group_session_id( m_groupSession, reinterpret_cast(sessionIdBuf.data()), sessionIdBuf.length()) - == olm_error()) { - throw lastError(); - } + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain the group session id"); + return sessionIdBuf; } diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 0b89349a..15979a05 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -17,9 +17,9 @@ class QUOTIENT_API QOlmInboundGroupSession public: ~QOlmInboundGroupSession(); //! Creates a new instance of `OlmInboundGroupSession`. - static std::unique_ptr create(const QByteArray& key); + static QOlmExpected create(const QByteArray& key); //! Import an inbound group session, from a previous export. - static std::unique_ptr import(const QByteArray& key); + static QOlmExpected import(const QByteArray& key); //! Serialises an `OlmInboundGroupSession` to encrypted Base64. QByteArray pickle(const PicklingMode& mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 3d176274..18af5569 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -39,8 +39,10 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() RandomBuffer(randomLength).bytes(), randomLength) == olm_error()) { - // FIXME: create the session object earlier and use lastError() - throw olm_outbound_group_session_last_error_code(olmOutboundGroupSession); + // FIXME: create the session object earlier + QOLM_INTERNAL_ERROR_X("Failed to initialise an outbound group session", + olm_outbound_group_session_last_error( + olmOutboundGroupSession)); } // FIXME: is it used anywhere? @@ -52,7 +54,7 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() return std::make_unique(olmOutboundGroupSession); } -QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const +QByteArray QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf( olm_pickle_outbound_group_session_length(m_groupSession), '\0'); @@ -61,7 +63,7 @@ QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mo key.length(), pickledBuf.data(), pickledBuf.length()) == olm_error()) - return lastErrorCode(); + QOLM_INTERNAL_ERROR("Failed to pickle the outbound group session"); key.clear(); return pickledBuf; @@ -77,6 +79,9 @@ QOlmExpected QOlmOutboundGroupSession::unpickle( pickled.length()) == olm_error()) { // FIXME: create the session object earlier and use lastError() + qWarning(E2EE) << "Failed to unpickle an outbound group session:" + << olm_outbound_group_session_last_error( + olmOutboundGroupSession); return olm_outbound_group_session_last_error_code( olmOutboundGroupSession); } @@ -90,8 +95,7 @@ QOlmExpected QOlmOutboundGroupSession::unpickle( return std::make_unique(olmOutboundGroupSession); } -QOlmExpected QOlmOutboundGroupSession::encrypt( - const QByteArray& plaintext) const +QByteArray QOlmOutboundGroupSession::encrypt(const QByteArray& plaintext) const { const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintext.length()); @@ -102,7 +106,7 @@ QOlmExpected QOlmOutboundGroupSession::encrypt( reinterpret_cast(messageBuf.data()), messageBuf.length()) == olm_error()) - return lastErrorCode(); + QOLM_INTERNAL_ERROR("Failed to encrypt a message"); return messageBuf; } @@ -120,12 +124,12 @@ QByteArray QOlmOutboundGroupSession::sessionId() const m_groupSession, reinterpret_cast(idBuffer.data()), idBuffer.length()) == olm_error()) - throw lastError(); + QOLM_INTERNAL_ERROR("Failed to obtain group session id"); return idBuffer; } -QOlmExpected QOlmOutboundGroupSession::sessionKey() const +QByteArray QOlmOutboundGroupSession::sessionKey() const { const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); QByteArray keyBuffer(keyMaxLength, '\0'); @@ -133,7 +137,7 @@ QOlmExpected QOlmOutboundGroupSession::sessionKey() const m_groupSession, reinterpret_cast(keyBuffer.data()), keyMaxLength) == olm_error()) - return lastErrorCode(); + QOLM_INTERNAL_ERROR("Failed to obtain group session key"); return keyBuffer; } diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index e5e73ddf..d36fbf69 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -20,14 +20,14 @@ public: //! Throw OlmError on errors static QOlmOutboundGroupSessionPtr create(); //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. - QOlmExpected pickle(const PicklingMode &mode) const; + QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( QByteArray&& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. - QOlmExpected encrypt(const QByteArray& plaintext) const; + QByteArray encrypt(const QByteArray& plaintext) const; //! Get the current message index for this session. //! @@ -42,7 +42,7 @@ public: //! //! Each message is sent with a different ratchet key. This function returns the //! ratchet key that will be used for the next message. - QOlmExpected sessionKey() const; + QByteArray sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); int messageCount() const; diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 7c102a96..0d6edd21 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -90,23 +90,23 @@ QOlmExpected QOlmSession::createOutboundSession( == olm_error()) { // FIXME: the QOlmSession object should be created earlier const auto lastErr = olm_session_last_error_code(olmOutboundSession); - if (lastErr == OLM_NOT_ENOUGH_RANDOM) { - throw lastErr; - } + QOLM_FAIL_OR_LOG_X(lastErr == OLM_NOT_ENOUGH_RANDOM, + "Failed to create an outbound Olm session", + olm_session_last_error(olmOutboundSession)); return lastErr; } return std::make_unique(olmOutboundSession); } -QOlmExpected QOlmSession::pickle(const PicklingMode &mode) const +QByteArray QOlmSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_session_length(m_session), '\0'); QByteArray key = toKey(mode); if (olm_pickle_session(m_session, key.data(), key.length(), pickledBuf.data(), pickledBuf.length()) == olm_error()) - return lastErrorCode(); + QOLM_INTERNAL_ERROR("Failed to pickle an Olm session"); key.clear(); return pickledBuf; @@ -121,7 +121,11 @@ QOlmExpected QOlmSession::unpickle(QByteArray&& pickled, pickled.data(), pickled.length()) == olm_error()) { // FIXME: the QOlmSession object should be created earlier - return olm_session_last_error_code(olmSession); + const auto errorCode = olm_session_last_error_code(olmSession); + QOLM_FAIL_OR_LOG_X(errorCode == OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to unpickle an Olm session", + olm_session_last_error(olmSession)); + return errorCode; } key.clear(); @@ -140,7 +144,7 @@ QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) RandomBuffer(randomLength), randomLength, messageBuf.data(), messageBuf.length()) == olm_error()) { - throw lastError(); + QOLM_INTERNAL_ERROR("Failed to encrypt the message"); } return QOlmMessage(messageBuf, QOlmMessage::Type(messageType)); @@ -159,7 +163,9 @@ QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const const auto plaintextMaxLen = olm_decrypt_max_plaintext_length( m_session, messageTypeValue, messageBuf.data(), messageBuf.length()); if (plaintextMaxLen == olm_error()) { - return lastError(); + qWarning(E2EE) << "Couldn't calculate decrypted message length:" + << lastError(); + return lastErrorCode(); } QByteArray plaintextBuf(plaintextMaxLen, '\0'); @@ -170,11 +176,9 @@ QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const olm_decrypt(m_session, messageTypeValue, messageBuf2.data(), messageBuf2.length(), plaintextBuf.data(), plaintextMaxLen); if (plaintextResultLen == olm_error()) { - const auto lastErr = lastErrorCode(); - if (lastErr == OLM_OUTPUT_BUFFER_TOO_SMALL) { - throw lastErr; - } - return lastErr; + QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to decrypt the message"); + return lastErrorCode(); } plaintextBuf.truncate(plaintextResultLen); return plaintextBuf; @@ -183,10 +187,10 @@ QOlmExpected QOlmSession::decrypt(const QOlmMessage &message) const QByteArray QOlmSession::sessionId() const { const auto idMaxLength = olm_session_id_length(m_session); - QByteArray idBuffer(idMaxLength, '0'); - if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error()) { - throw lastError(); - } + QByteArray idBuffer(idMaxLength, '\0'); + if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain Olm session id"); + return idBuffer; } @@ -203,8 +207,8 @@ bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); if (maybeMatches == olm_error()) - qWarning(E2EE) << "Error matching an inbound session:" - << olm_session_last_error(m_session); + qWarning(E2EE) << "Error matching an inbound session:" << lastError(); + return maybeMatches == 1; // Any errors are treated as non-match } @@ -218,8 +222,8 @@ bool QOlmSession::matchesInboundSessionFrom( oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); if (maybeMatches == olm_error()) - qCWarning(E2EE) << "Error matching an inbound session:" - << olm_session_last_error(m_session); + qCWarning(E2EE) << "Error matching an inbound session:" << lastError(); + return maybeMatches == 1; } diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index 174e98ef..400fb854 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -30,7 +30,7 @@ public: const QByteArray& theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. - QOlmExpected pickle(const PicklingMode &mode) const; + QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 previously made with pickle() static QOlmExpected unpickle(QByteArray&& pickled, diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h index da9d2d18..17eee7a3 100644 --- a/lib/e2ee/qolmutils.h +++ b/lib/e2ee/qolmutils.h @@ -34,4 +34,22 @@ public: return RandomBuffer(bufferSize); } +#define QOLM_INTERNAL_ERROR_X(Message_, LastError_) \ + qFatal("%s, internal error: %s", Message_, LastError_) + +#define QOLM_INTERNAL_ERROR(Message_) \ + QOLM_INTERNAL_ERROR_X(Message_, lastError()) + +#define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_) \ + do { \ + const QString errorMsg{ (Message_) }; \ + if (InternalCondition_) \ + QOLM_INTERNAL_ERROR_X(qPrintable(errorMsg), (LastErrorText_)); \ + qWarning(E2EE).nospace() << errorMsg << ": " << (LastErrorText_); \ + } while (false) /* End of macro */ + +#define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \ + QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), \ + lastError()) + } // namespace Quotient 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 d5c1afc536dde87f460ccadfcfd51444b5e9bb82 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 22 Sep 2022 14:01:01 +0200 Subject: Trample Sonar warnings --- lib/connection.cpp | 4 +++- lib/e2ee/qolmaccount.cpp | 7 ++++--- lib/e2ee/qolminboundsession.cpp | 2 +- lib/e2ee/qolminboundsession.h | 2 +- lib/e2ee/qolmoutboundsession.cpp | 6 +++--- lib/e2ee/qolmsession.cpp | 13 ++++++------- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 0afbffbc..51c2062f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -648,7 +648,9 @@ void Connection::Private::completeSetup(const QString& mxId) }); } else { // account already existing - olmAccount->unpickle(database->accountPickle(), picklingMode); + if (!olmAccount->unpickle(database->accountPickle(), picklingMode)) + qWarning(E2EE) + << "Could not unpickle Olm account, E2EE won't be available"; } #endif // Quotient_E2EE_ENABLED emit q->stateChanged(); diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index d03bcb3b..7392a9f5 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -43,15 +43,16 @@ QOlmAccount::~QOlmAccount() void QOlmAccount::createNewAccount() { m_account = olm_account(new uint8_t[olm_account_size()]); - const auto randomLength = olm_create_account_random_length(m_account); - if (olm_create_account(m_account, RandomBuffer(randomLength), randomLength) + if (const auto randomLength = olm_create_account_random_length(m_account); + olm_create_account(m_account, RandomBuffer(randomLength), randomLength) == olm_error()) QOLM_INTERNAL_ERROR("Failed to create a new account"); emit needsSave(); } -OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, const PicklingMode &mode) +OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, + const PicklingMode& mode) { m_account = olm_account(new uint8_t[olm_account_size()]); if (const auto key = toKey(mode); diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 1e3803f2..18275dc0 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -49,7 +49,7 @@ QOlmExpected QOlmInboundGroupSession::create( return std::make_unique(olmInboundGroupSession); } -QOlmExpected QOlmInboundGroupSession::import( +QOlmExpected QOlmInboundGroupSession::importSession( const QByteArray& key) { const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 15979a05..b9710354 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -19,7 +19,7 @@ public: //! Creates a new instance of `OlmInboundGroupSession`. static QOlmExpected create(const QByteArray& key); //! Import an inbound group session, from a previous export. - static QOlmExpected import(const QByteArray& key); + static QOlmExpected importSession(const QByteArray& key); //! Serialises an `OlmInboundGroupSession` to encrypted Base64. QByteArray pickle(const PicklingMode& mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 18af5569..56f78906 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -33,9 +33,9 @@ QOlmOutboundGroupSession::~QOlmOutboundGroupSession() QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() { auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); - const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); - - if (olm_init_outbound_group_session(olmOutboundGroupSession, + if (const auto randomLength = olm_init_outbound_group_session_random_length( + olmOutboundGroupSession); + olm_init_outbound_group_session(olmOutboundGroupSession, RandomBuffer(randomLength).bytes(), randomLength) == olm_error()) { diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index 0d6edd21..e3f69132 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -80,10 +80,9 @@ QOlmExpected QOlmSession::createOutboundSession( const QByteArray& theirOneTimeKey) { auto* olmOutboundSession = create(); - const auto randomLength = - olm_create_outbound_session_random_length(olmOutboundSession); - - if (olm_create_outbound_session( + if (const auto randomLength = + olm_create_outbound_session_random_length(olmOutboundSession); + olm_create_outbound_session( olmOutboundSession, account->data(), theirIdentityKey.data(), theirIdentityKey.length(), theirOneTimeKey.data(), theirOneTimeKey.length(), RandomBuffer(randomLength), randomLength) @@ -136,11 +135,11 @@ QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) { const auto messageMaxLength = olm_encrypt_message_length(m_session, plaintext.length()); - QByteArray messageBuf(messageMaxLength, '0'); + QByteArray messageBuf(messageMaxLength, '\0'); // NB: The type has to be calculated before calling olm_encrypt() const auto messageType = olm_encrypt_message_type(m_session); - const auto randomLength = olm_encrypt_random_length(m_session); - if (olm_encrypt(m_session, plaintext.data(), plaintext.length(), + if (const auto randomLength = olm_encrypt_random_length(m_session); + olm_encrypt(m_session, plaintext.data(), plaintext.length(), RandomBuffer(randomLength), randomLength, messageBuf.data(), messageBuf.length()) == olm_error()) { -- 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/e2ee/qolmoutboundsession.cpp | 11 ----------- lib/room.cpp | 1 - 2 files changed, 12 deletions(-) diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 56f78906..1176d790 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -45,12 +45,6 @@ QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() olmOutboundGroupSession)); } - // FIXME: is it used anywhere? - const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession); - QByteArray keyBuffer(keyMaxLength, '\0'); - olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast(keyBuffer.data()), - keyMaxLength); - return std::make_unique(olmOutboundGroupSession); } @@ -85,11 +79,6 @@ QOlmExpected QOlmOutboundGroupSession::unpickle( return olm_outbound_group_session_last_error_code( olmOutboundGroupSession); } - const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession); - QByteArray idBuffer(idMaxLength, '\0'); - olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast(idBuffer.data()), - idBuffer.length()); - // FIXME: idBuffer doesn't seem to be used, is it needed here? key.clear(); return std::make_unique(olmOutboundGroupSession); 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 From 72e14cb1bdff68dfe0fb61fff0defd6c50dff43c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 25 Sep 2022 21:37:49 +0200 Subject: QOlmUtility::ed25519Verify: just return bool It's too easy to incorrectly test the previous return type. (cherry picked from commit 5904a61c59f0eef00aef07ef998658fd791ff139) --- autotests/testolmaccount.cpp | 3 +-- autotests/testolmutility.cpp | 6 ++---- lib/e2ee/qolmaccount.cpp | 3 +-- lib/e2ee/qolmutility.cpp | 21 +++++---------------- lib/e2ee/qolmutility.h | 4 ++-- 5 files changed, 11 insertions(+), 26 deletions(-) diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp index 56532698..53a0c955 100644 --- a/autotests/testolmaccount.cpp +++ b/autotests/testolmaccount.cpp @@ -60,8 +60,7 @@ void TestOlmAccount::signatureValid() QOlmUtility utility; const auto identityKeys = olmAccount.identityKeys(); const auto ed25519Key = identityKeys.ed25519; - const auto verify = utility.ed25519Verify(ed25519Key, message, signature); - QVERIFY(verify.value_or(false)); + QVERIFY(utility.ed25519Verify(ed25519Key, message, signature)); } void TestOlmAccount::oneTimeKeysValid() diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp index 64ceb3e7..4de5afdf 100644 --- a/autotests/testolmutility.cpp +++ b/autotests/testolmutility.cpp @@ -78,10 +78,8 @@ void TestOlmUtility::verifySignedOneTimeKey() delete[](reinterpret_cast(utility)); QOlmUtility utility2; - auto res2 = - utility2 - .ed25519Verify(aliceOlm.identityKeys().ed25519, msg, signatureBuf1) - .value_or(false); + auto res2 = utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg, + signatureBuf1); //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); QVERIFY(res2); diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 7392a9f5..345ab16b 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -269,8 +269,7 @@ bool Quotient::ed25519VerifySignature(const QString& signingKey, QByteArray signingKeyBuf = signingKey.toUtf8(); QOlmUtility utility; auto signatureBuf = signature.toUtf8(); - return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf) - .value_or(false); + return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); } QString QOlmAccount::accountId() const { return m_userId % '/' % m_deviceId; } diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 08c2b699..46f7f4f3 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -44,21 +44,10 @@ QString QOlmUtility::sha256Utf8Msg(const QString &message) const return sha256Bytes(message.toUtf8()); } -QOlmExpected QOlmUtility::ed25519Verify(const QByteArray& key, - const QByteArray& message, - const QByteArray& signature) +bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message, + QByteArray signature) { - QByteArray signatureBuf(signature.length(), '\0'); - std::copy(signature.begin(), signature.end(), signatureBuf.begin()); - - if (olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), - message.size(), signatureBuf.data(), - signatureBuf.size()) - == 0) - return true; - - auto error = lastErrorCode(); - if (error == OLM_BAD_MESSAGE_MAC) - return false; - return error; + return olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), + message.size(), signature.data(), signature.size()) + == 0; } diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index 89277385..508767bf 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -29,8 +29,8 @@ public: //! \param key QByteArray The public part of the ed25519 key that signed the message. //! \param message QByteArray The message that was signed. //! \param signature QByteArray The signature of the message. - QOlmExpected ed25519Verify(const QByteArray &key, - const QByteArray &message, const QByteArray &signature); + bool ed25519Verify(const QByteArray &key, + const QByteArray &message, QByteArray signature); OlmErrorCode lastErrorCode() const; const char* lastError() const; -- cgit v1.2.3 From 40b29069f55b551a67f4244c2b803b16e8287cd2 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 1 Oct 2022 20:11:26 +0100 Subject: Relax Omittable::operator->/operator* The imposed limitations made mutation of Omittables pretty much impossible outside of rvalues. Honestly, it was pretty much the original intention - and it doesn't look right, in retrospect. (cherry picked from commit 7f886b34936f324662eb17afbb214d4800dcea03) --- lib/omittable.h | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/lib/omittable.h b/lib/omittable.h index b5efecf5..0718aaff 100644 --- a/lib/omittable.h +++ b/lib/omittable.h @@ -26,12 +26,17 @@ constexpr auto none = std::nullopt; //! \return Always an Omittable: if \p fn returns another type, lift() wraps //! it in an Omittable; if \p fn returns an Omittable, that return value //! (or none) is returned as is. -template -inline auto lift(FnT&& fn, MaybeTs&&... args) +template +inline auto lift(FnT&& fn, ArgTs&&... args) { - return (... && bool(args)) - ? Omittable(std::invoke(std::forward(fn), *args...)) - : none; + if constexpr (std::is_void_v(fn), + *args...))>) { + if ((... && bool(args))) + std::invoke(std::forward(fn), *args...); + } else + return (... && bool(args)) + ? Omittable(std::invoke(std::forward(fn), *args...)) + : none; } /** `std::optional` with tweaks @@ -49,10 +54,8 @@ inline auto lift(FnT&& fn, MaybeTs&&... args) * have it; but besides that, value_or() or (after explicit checking) * `operator*()`/`operator->()` are better alternatives within Quotient * that doesn't practice throwing exceptions (as doesn't most of Qt). - * - disabled non-const lvalue operator*() and operator->(), as it's too easy - * to inadvertently cause a value change through them. - * - ensure() to provide a safe and explicit lvalue accessor instead of - * those above. Allows chained initialisation of nested Omittables: + * - ensure() to provide a safer lvalue accessor instead of operator* or + * operator->. Allows chained initialisation of nested Omittables: * \code * struct Inner { int member = 10; Omittable innermost; }; * struct Outer { int anotherMember = 10; Omittable inner; }; @@ -95,7 +98,7 @@ public: // with operator-> or operator* // The technical reason is that Xcode 10 has incomplete std::optional // that has no value(); but using value() may also mean that you rely - // on the optional throwing an exception (which is not assumed practice + // on the optional throwing an exception (which is not an assumed practice // throughout Quotient) or that you spend unnecessary CPU cycles on // an extraneous has_value() check. auto& value() = delete; @@ -131,15 +134,6 @@ public: return true; } - // Hide non-const lvalue operator-> and operator* as these are - // a bit too surprising: value() & doesn't lazy-create an object; - // and it's too easy to inadvertently change the underlying value. - - const value_type* operator->() const& { return base_type::operator->(); } - value_type* operator->() && { return base_type::operator->(); } - const value_type& operator*() const& { return base_type::operator*(); } - value_type& operator*() && { return base_type::operator*(); } - // The below is inspired by the proposed std::optional monadic operations // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html). @@ -162,7 +156,7 @@ public: //! returning Omittable, T2 is any supported type //! \sa then_or, transform template - auto then(FnT&& fn) const& + auto then(FnT&& fn) const { return lift(std::forward(fn), *this); } @@ -171,7 +165,7 @@ public: //! //! This is an rvalue overload for then(). template - auto then(FnT&& fn) && + auto then(FnT&& fn) { return lift(std::forward(fn), *this); } @@ -184,7 +178,7 @@ public: //! an operation on an Omittable without having to deal with another //! Omittable afterwards. template - auto then_or(FnT&& fn, FallbackT&& fallback) const& + auto then_or(FnT&& fn, FallbackT&& fallback) const { return then(std::forward(fn)) .value_or(std::forward(fallback)); @@ -194,7 +188,7 @@ public: //! //! This is an overload for functions that accept rvalue template - auto then_or(FnT&& fn, FallbackT&& fallback) && + auto then_or(FnT&& fn, FallbackT&& fallback) { return then(std::forward(fn)) .value_or(std::forward(fallback)); -- cgit v1.2.3 From bcae903921b81f578b0717e7376a45f9cafa16ad Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 3 Oct 2022 21:02:22 +0200 Subject: Regenerate API files from spec v1.4 --- lib/csapi/account-data.h | 61 ++++++++++----------- lib/csapi/definitions/public_rooms_response.h | 6 +++ lib/csapi/definitions/room_event_filter.h | 9 ++++ lib/csapi/joining.h | 2 +- lib/csapi/keys.h | 4 +- lib/csapi/list_public_rooms.h | 10 +++- lib/csapi/login.cpp | 3 ++ lib/csapi/read_markers.cpp | 7 ++- lib/csapi/read_markers.h | 11 +++- lib/csapi/receipts.h | 8 ++- lib/csapi/redaction.h | 6 +-- lib/csapi/relations.cpp | 45 +++++++++------- lib/csapi/relations.h | 55 +++++++++++++------ lib/csapi/room_send.h | 7 +-- lib/csapi/threads_list.cpp | 37 +++++++++++++ lib/csapi/threads_list.h | 76 +++++++++++++++++++++++++++ lib/csapi/to_device.h | 7 +-- 17 files changed, 269 insertions(+), 85 deletions(-) create mode 100644 lib/csapi/threads_list.cpp create mode 100644 lib/csapi/threads_list.h diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index 5140d340..70d4e492 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -8,46 +8,47 @@ namespace Quotient { -/*! \brief Set some account_data for the user. +/*! \brief Set some account data for the user. * - * Set some account_data for the client. This config is only visible to the user - * that set the account_data. The config will be synced to clients in the - * top-level `account_data`. + * Set some account data for the client. This config is only visible to the user + * that set the account data. The config will be available to clients through + * the top-level `account_data` field in the homeserver response to + * [/sync](#get_matrixclientv3sync). */ class QUOTIENT_API SetAccountDataJob : public BaseJob { public: - /*! \brief Set some account_data for the user. + /*! \brief Set some account data for the user. * * \param userId - * The ID of the user to set account_data for. The access token must be + * The ID of the user to set account data for. The access token must be * authorized to make requests for this user ID. * * \param type - * The event type of the account_data to set. Custom types should be + * The event type of the account data to set. Custom types should be * namespaced to avoid clashes. * * \param content - * The content of the account_data + * The content of the account data. */ explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); }; -/*! \brief Get some account_data for the user. +/*! \brief Get some account data for the user. * - * Get some account_data for the client. This config is only visible to the user - * that set the account_data. + * Get some account data for the client. This config is only visible to the user + * that set the account data. */ class QUOTIENT_API GetAccountDataJob : public BaseJob { public: - /*! \brief Get some account_data for the user. + /*! \brief Get some account data for the user. * * \param userId - * The ID of the user to get account_data for. The access token must be + * The ID of the user to get account data for. The access token must be * authorized to make requests for this user ID. * * \param type - * The event type of the account_data to get. Custom types should be + * The event type of the account data to get. Custom types should be * namespaced to avoid clashes. */ explicit GetAccountDataJob(const QString& userId, const QString& type); @@ -61,53 +62,53 @@ public: const QString& type); }; -/*! \brief Set some account_data for the user. +/*! \brief Set some account data for the user that is specific to a room. * - * Set some account_data for the client on a given room. This config is only - * visible to the user that set the account_data. The config will be synced to - * clients in the per-room `account_data`. + * Set some account data for the client on a given room. This config is only + * visible to the user that set the account data. The config will be delivered + * to clients in the per-room entries via [/sync](#get_matrixclientv3sync). */ class QUOTIENT_API SetAccountDataPerRoomJob : public BaseJob { public: - /*! \brief Set some account_data for the user. + /*! \brief Set some account data for the user that is specific to a room. * * \param userId - * The ID of the user to set account_data for. The access token must be + * The ID of the user to set account data for. The access token must be * authorized to make requests for this user ID. * * \param roomId - * The ID of the room to set account_data on. + * The ID of the room to set account data on. * * \param type - * The event type of the account_data to set. Custom types should be + * The event type of the account data to set. Custom types should be * namespaced to avoid clashes. * * \param content - * The content of the account_data + * The content of the account data. */ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); }; -/*! \brief Get some account_data for the user. +/*! \brief Get some account data for the user that is specific to a room. * - * Get some account_data for the client on a given room. This config is only - * visible to the user that set the account_data. + * Get some account data for the client on a given room. This config is only + * visible to the user that set the account data. */ class QUOTIENT_API GetAccountDataPerRoomJob : public BaseJob { public: - /*! \brief Get some account_data for the user. + /*! \brief Get some account data for the user that is specific to a room. * * \param userId - * The ID of the user to set account_data for. The access token must be + * The ID of the user to get account data for. The access token must be * authorized to make requests for this user ID. * * \param roomId - * The ID of the room to get account_data for. + * The ID of the room to get account data for. * * \param type - * The event type of the account_data to get. Custom types should be + * The event type of the account data to get. Custom types should be * namespaced to avoid clashes. */ explicit GetAccountDataPerRoomJob(const QString& userId, diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index d0a2595c..7c7d9cc6 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -35,6 +35,10 @@ struct PublicRoomsChunk { /// The URL for the room's avatar, if one is set. QUrl avatarUrl; + /// The `type` of room (from + /// [`m.room.create`](/client-server-api/#mroomcreate)), if any. + QString roomType; + /// The room's join rule. When not present, the room is assumed to /// be `public`. Note that rooms with `invite` join rules are not /// expected here, but rooms with `knock` rules are given their @@ -56,6 +60,7 @@ struct JsonObjectConverter { addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); + addParam(jo, QStringLiteral("room_type"), pod.roomType); addParam(jo, QStringLiteral("join_rule"), pod.joinRule); } static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) @@ -68,6 +73,7 @@ struct JsonObjectConverter { fromJson(jo.value("world_readable"_ls), pod.worldReadable); fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); + fromJson(jo.value("room_type"_ls), pod.roomType); fromJson(jo.value("join_rule"_ls), pod.joinRule); } }; diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 91caf667..293e5492 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -11,6 +11,11 @@ namespace Quotient { struct RoomEventFilter : EventFilter { + /// If `true`, enables per-[thread](/client-server-api/#threading) + /// notification counts. Only applies to the `/sync` endpoint. Defaults to + /// `false`. + Omittable unreadThreadNotifications; + /// If `true`, enables lazy-loading of membership events. See /// [Lazy-loading room /// members](/client-server-api/#lazy-loading-room-members) for more @@ -44,6 +49,8 @@ struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod) { fillJson(jo, pod); + addParam(jo, QStringLiteral("unread_thread_notifications"), + pod.unreadThreadNotifications); addParam(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); addParam(jo, QStringLiteral("include_redundant_members"), @@ -56,6 +63,8 @@ struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod) { fillFromJson(jo, pod); + fromJson(jo.value("unread_thread_notifications"_ls), + pod.unreadThreadNotifications); fromJson(jo.value("lazy_load_members"_ls), pod.lazyLoadMembers); fromJson(jo.value("include_redundant_members"_ls), pod.includeRedundantMembers); diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 233537bb..c86baa90 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -55,7 +55,7 @@ public: /*! \brief Start the requesting user participating in a particular room. * * *Note that this API takes either a room ID or alias, unlike* - * `/room/{roomId}/join`. + * `/rooms/{roomId}/join`. * * This API starts a user participating in a particular room, if that user * is allowed to participate in that room. After this call, the client is diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 2f2ebc6d..b28de305 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,11 +4,11 @@ #pragma once -#include "e2ee/e2ee.h" - #include "csapi/definitions/cross_signing_key.h" #include "csapi/definitions/device_keys.h" +#include "e2ee/e2ee.h" + #include "jobs/basejob.h" namespace Quotient { diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index e1f03db7..3b6b91b9 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -139,9 +139,14 @@ public: /// Filter to apply to the results. struct Filter { - /// A string to search for in the room metadata, e.g. name, - /// topic, canonical alias etc. (Optional). + /// An optional string to search for in the room metadata, e.g. name, + /// topic, canonical alias, etc. QString genericSearchTerm; + /// An optional list of [room types](/client-server-api/#types) to + /// search for. To include rooms without a room type, specify `null` + /// within this list. When not specified, all applicable rooms + /// (regardless of type) are returned. + QStringList roomTypes; }; // Construction/destruction @@ -211,6 +216,7 @@ struct JsonObjectConverter { { addParam(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); + addParam(jo, QStringLiteral("room_types"), pod.roomTypes); } }; diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 81e603b5..7bb74e29 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -38,4 +38,7 @@ LoginJob::LoginJob(const QString& type, addParam(_dataJson, QStringLiteral("refresh_token"), refreshToken); setRequestData({ _dataJson }); + addExpectedKey("user_id"); + addExpectedKey("access_token"); + addExpectedKey("device_id"); } diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index de5f4a9a..febd6d3a 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -8,12 +8,15 @@ using namespace Quotient; SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, - const QString& mRead) + const QString& mRead, + const QString& mReadPrivate) : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), makePath("/_matrix/client/v3", "/rooms/", roomId, "/read_markers")) { QJsonObject _dataJson; - addParam<>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead); + addParam(_dataJson, QStringLiteral("m.fully_read"), mFullyRead); addParam(_dataJson, QStringLiteral("m.read"), mRead); + addParam(_dataJson, QStringLiteral("m.read.private"), + mReadPrivate); setRequestData({ _dataJson }); } diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index d13fa4fc..1024076f 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -28,9 +28,16 @@ public: * The event ID to set the read receipt location at. This is * equivalent to calling `/receipt/m.read/$elsewhere:example.org` * and is provided here to save that extra call. + * + * \param mReadPrivate + * The event ID to set the *private* read receipt location at. This + * equivalent to calling `/receipt/m.read.private/$elsewhere:example.org` + * and is provided here to save that extra call. */ - explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, - const QString& mRead = {}); + explicit SetReadMarkerJob(const QString& roomId, + const QString& mFullyRead = {}, + const QString& mRead = {}, + const QString& mReadPrivate = {}); }; } // namespace Quotient diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index e29e7b29..98bc5004 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -21,7 +21,13 @@ public: * The room in which to send the event. * * \param receiptType - * The type of receipt to send. + * The type of receipt to send. This can also be `m.fully_read` as an + * alternative to + * [`/read_makers`](/client-server-api/#post_matrixclientv3roomsroomidread_markers). + * + * Note that `m.fully_read` does not appear under `m.receipt`: this + * endpoint effectively calls `/read_markers` internally when presented with + * a receipt type of `m.fully_read`. * * \param eventId * The event ID to acknowledge up to. diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index 29d9c5d5..2f85793e 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -33,9 +33,9 @@ public: * The ID of the event to redact * * \param txnId - * The transaction ID for this event. Clients should generate a - * unique ID; it will be used by the server to ensure idempotency of - * requests. + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate a unique ID; it will be used by the + * server to ensure idempotency of requests. * * \param reason * The reason for the event being redacted. diff --git a/lib/csapi/relations.cpp b/lib/csapi/relations.cpp index 8bcecee4..1d8febcc 100644 --- a/lib/csapi/relations.cpp +++ b/lib/csapi/relations.cpp @@ -7,105 +7,112 @@ using namespace Quotient; auto queryToGetRelatingEvents(const QString& from, const QString& to, - Omittable limit) + Omittable limit, const QString& dir) { QUrlQuery _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("to"), to); addParam(_q, QStringLiteral("limit"), limit); + addParam(_q, QStringLiteral("dir"), dir); return _q; } QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, const QString& from, const QString& to, - Omittable limit) + Omittable limit, + const QString& dir) { return BaseJob::makeRequestUrl(std::move(baseUrl), makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", eventId), - queryToGetRelatingEvents(from, to, limit)); + queryToGetRelatingEvents(from, to, limit, + dir)); } -GetRelatingEventsJob::GetRelatingEventsJob(const QString& roomId, - const QString& eventId, - const QString& from, - const QString& to, - Omittable limit) +GetRelatingEventsJob::GetRelatingEventsJob( + const QString& roomId, const QString& eventId, const QString& from, + const QString& to, Omittable limit, const QString& dir) : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"), makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", eventId), - queryToGetRelatingEvents(from, to, limit)) + queryToGetRelatingEvents(from, to, limit, dir)) { addExpectedKey("chunk"); } auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to, - Omittable limit) + Omittable limit, + const QString& dir) { QUrlQuery _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("to"), to); addParam(_q, QStringLiteral("limit"), limit); + addParam(_q, QStringLiteral("dir"), dir); return _q; } QUrl GetRelatingEventsWithRelTypeJob::makeRequestUrl( QUrl baseUrl, const QString& roomId, const QString& eventId, const QString& relType, const QString& from, const QString& to, - Omittable limit) + Omittable limit, const QString& dir) { return BaseJob::makeRequestUrl( std::move(baseUrl), makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", eventId, "/", relType), - queryToGetRelatingEventsWithRelType(from, to, limit)); + queryToGetRelatingEventsWithRelType(from, to, limit, dir)); } GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob( const QString& roomId, const QString& eventId, const QString& relType, - const QString& from, const QString& to, Omittable limit) + const QString& from, const QString& to, Omittable limit, + const QString& dir) : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"), makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", eventId, "/", relType), - queryToGetRelatingEventsWithRelType(from, to, limit)) + queryToGetRelatingEventsWithRelType(from, to, limit, dir)) { addExpectedKey("chunk"); } auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from, const QString& to, - Omittable limit) + Omittable limit, + const QString& dir) { QUrlQuery _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("to"), to); addParam(_q, QStringLiteral("limit"), limit); + addParam(_q, QStringLiteral("dir"), dir); return _q; } QUrl GetRelatingEventsWithRelTypeAndEventTypeJob::makeRequestUrl( QUrl baseUrl, const QString& roomId, const QString& eventId, const QString& relType, const QString& eventType, const QString& from, - const QString& to, Omittable limit) + const QString& to, Omittable limit, const QString& dir) { return BaseJob::makeRequestUrl( std::move(baseUrl), makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", eventId, "/", relType, "/", eventType), - queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit)); + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, dir)); } GetRelatingEventsWithRelTypeAndEventTypeJob:: GetRelatingEventsWithRelTypeAndEventTypeJob( const QString& roomId, const QString& eventId, const QString& relType, const QString& eventType, const QString& from, const QString& to, - Omittable limit) + Omittable limit, const QString& dir) : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"), makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", eventId, "/", relType, "/", eventType), - queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit)) + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, + dir)) { addExpectedKey("chunk"); } diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h index 794ae445..5d6efd1c 100644 --- a/lib/csapi/relations.h +++ b/lib/csapi/relations.h @@ -36,8 +36,8 @@ public: * The pagination token to start returning results from. If not supplied, * results start at the most recent topological event known to the server. * - * Can be a `next_batch` token from a previous call, or a returned - * `start` token from + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), * or a `next_batch` token from * [`/sync`](/client-server-api/#get_matrixclientv3sync). @@ -55,11 +55,18 @@ public: * responses. * * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. */ explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId, const QString& from = {}, const QString& to = {}, - Omittable limit = none); + Omittable limit = none, + const QString& dir = {}); /*! \brief Construct a URL without creating a full-fledged job object * @@ -69,7 +76,8 @@ public: static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, const QString& from = {}, const QString& to = {}, - Omittable limit = none); + Omittable limit = none, + const QString& dir = {}); // Result properties @@ -122,8 +130,8 @@ public: * The pagination token to start returning results from. If not supplied, * results start at the most recent topological event known to the server. * - * Can be a `next_batch` token from a previous call, or a returned - * `start` token from + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), * or a `next_batch` token from * [`/sync`](/client-server-api/#get_matrixclientv3sync). @@ -141,13 +149,17 @@ public: * responses. * * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. */ - explicit GetRelatingEventsWithRelTypeJob(const QString& roomId, - const QString& eventId, - const QString& relType, - const QString& from = {}, - const QString& to = {}, - Omittable limit = none); + explicit GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from = {}, const QString& to = {}, + Omittable limit = none, const QString& dir = {}); /*! \brief Construct a URL without creating a full-fledged job object * @@ -157,7 +169,8 @@ public: static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, const QString& relType, const QString& from = {}, const QString& to = {}, - Omittable limit = none); + Omittable limit = none, + const QString& dir = {}); // Result properties @@ -219,8 +232,8 @@ public: * The pagination token to start returning results from. If not supplied, * results start at the most recent topological event known to the server. * - * Can be a `next_batch` token from a previous call, or a returned - * `start` token from + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), * or a `next_batch` token from * [`/sync`](/client-server-api/#get_matrixclientv3sync). @@ -238,11 +251,18 @@ public: * responses. * * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. */ explicit GetRelatingEventsWithRelTypeAndEventTypeJob( const QString& roomId, const QString& eventId, const QString& relType, const QString& eventType, const QString& from = {}, - const QString& to = {}, Omittable limit = none); + const QString& to = {}, Omittable limit = none, + const QString& dir = {}); /*! \brief Construct a URL without creating a full-fledged job object * @@ -254,7 +274,8 @@ public: const QString& eventId, const QString& relType, const QString& eventType, const QString& from = {}, const QString& to = {}, - Omittable limit = none); + Omittable limit = none, + const QString& dir = {}); // Result properties diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index fcb6b24f..abe5f207 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -30,9 +30,10 @@ public: * The type of event to send. * * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate an ID unique across requests with the + * same access token; it will be used by the server to ensure idempotency of + * requests. * * \param body * This endpoint is used to send a message event to a room. Message events diff --git a/lib/csapi/threads_list.cpp b/lib/csapi/threads_list.cpp new file mode 100644 index 00000000..26924f24 --- /dev/null +++ b/lib/csapi/threads_list.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "threads_list.h" + +using namespace Quotient; + +auto queryToGetThreadRoots(const QString& include, Omittable limit, + const QString& from) +{ + QUrlQuery _q; + addParam(_q, QStringLiteral("include"), include); + addParam(_q, QStringLiteral("limit"), limit); + addParam(_q, QStringLiteral("from"), from); + return _q; +} + +QUrl GetThreadRootsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& include, + Omittable limit, const QString& from) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/threads"), + queryToGetThreadRoots(include, limit, from)); +} + +GetThreadRootsJob::GetThreadRootsJob(const QString& roomId, + const QString& include, + Omittable limit, const QString& from) + : BaseJob(HttpVerb::Get, QStringLiteral("GetThreadRootsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/threads"), + queryToGetThreadRoots(include, limit, from)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/threads_list.h b/lib/csapi/threads_list.h new file mode 100644 index 00000000..7041583a --- /dev/null +++ b/lib/csapi/threads_list.h @@ -0,0 +1,76 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/roomevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Retrieve a list of threads in a room, with optional filters. + * + * Paginates over the thread roots in a room, ordered by the `latest_event` of + * each thread root in its bundle. + */ +class QUOTIENT_API GetThreadRootsJob : public BaseJob { +public: + /*! \brief Retrieve a list of threads in a room, with optional filters. + * + * \param roomId + * The room ID where the thread roots are located. + * + * \param include + * Optional (default `all`) flag to denote which thread roots are of + * interest to the caller. When `all`, all thread roots found in the room + * are returned. When `participated`, only thread roots for threads the user + * has [participated + * in](/client-server-api/#server-side-aggreagtion-of-mthread-relationships) + * will be returned. + * + * \param limit + * Optional limit for the maximum number of thread roots to include per + * response. Must be an integer greater than zero. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param from + * A pagination token from a previous result. When not provided, the + * server starts paginating from the most recent event visible to the user + * (as per history visibility rules; topologically). + */ + explicit GetThreadRootsJob(const QString& roomId, + const QString& include = {}, + Omittable limit = none, + const QString& from = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetThreadRootsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& include = {}, + Omittable limit = none, + const QString& from = {}); + + // Result properties + + /// The thread roots, ordered by the `latest_event` in each event's + /// aggregation bundle. All events returned include bundled + /// [aggregations](/client-server-api/#aggregations). + /// + /// If the thread root event was sent by an [ignored + /// user](/client-server-api/#ignoring-users), the event is returned + /// redacted to the caller. This is to simulate the same behaviour of a + /// client doing aggregation locally on the thread. + RoomEvents chunk() { return takeFromJson("chunk"_ls); } + + /// A token to supply to `from` to keep paginating the responses. Not + /// present when there are no further results. + QString nextBatch() const { return loadFromJson("next_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index 5b6e0bfb..54828337 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -21,9 +21,10 @@ public: * The type of event to send. * * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate an ID unique across requests with the + * same access token; it will be used by the server to ensure idempotency of + * requests. * * \param messages * The messages to send. A map from user ID, to a map from -- cgit v1.2.3 From 6824a0ab5614c5497b220eb9e18e190cd57d5710 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 6 Oct 2022 12:27:42 +0200 Subject: Properly export KeyVerificationKeyEvent --- lib/events/keyverificationevent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 0e939508..80aebcf3 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -207,7 +207,7 @@ public: /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. -class KeyVerificationKeyEvent : public KeyVerificationEvent { +class QUOTIENT_API KeyVerificationKeyEvent : public KeyVerificationEvent { public: QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key") -- cgit v1.2.3