From c3149cecf3bee2ae4360d5d46dffd5fc4330539e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 11:01:57 +0100 Subject: .travis.yml: be explicit about using trusty Travis CI switched to xenial by default since some time ago. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 3aaa4039..0d314cf4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: cpp +dist: trusty addons: apt: -- cgit v1.2.3 From 80c1747137a3f799a20c4d1f3d1a2baa72084adb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 2 Nov 2019 16:20:57 +0900 Subject: Compatibility with Qt 5.14 --- lib/converters.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/converters.h b/lib/converters.h index af2be645..5c31b93d 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -37,6 +37,7 @@ template using optional = std::experimental::optional; #endif +#if QT_VERSION < QT_VERSION_CHECK(5,14,0) // Enable std::unordered_map namespace std { @@ -51,7 +52,8 @@ namespace std ); } }; -} +} // namespace std +#endif class QVariant; -- cgit v1.2.3 From 9e58bbe349205ded067fdc92c221fa128a05820f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 22 Mar 2020 21:05:21 +0100 Subject: Fix missing alias updates; make Room aliases accessors work (using MSC2432) Backport of #383 to 0.5.x branch. Fixes #301. --- lib/room.cpp | 54 +++++++++++++++++++++++++++++++++++++++++++----------- lib/room.h | 1 + 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 9e7ff8d2..0942d730 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -377,7 +377,17 @@ QString Room::name() const QStringList Room::aliases() const { - return d->getCurrentState()->aliases(); + const auto* evt = d->getCurrentState(); + auto aliases = fromJson(evt->contentJson()["alt_aliases"]); + if (!evt->alias().isEmpty()) + aliases << evt->alias(); + return aliases; +} + +QStringList Room::altAliases() const +{ + const auto* evt = d->getCurrentState(); + return fromJson(evt->contentJson()["alt_aliases"]); } QString Room::canonicalAlias() const @@ -1331,7 +1341,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (roomChanges&TopicChange) emit topicChanged(); - if (roomChanges&NameChange) + if (roomChanges&(NameChange|CanonicalAliasChange)) emit namesChanged(this); if (roomChanges&MembersChange) @@ -1608,12 +1618,18 @@ void Room::setName(const QString& newName) void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState(RoomCanonicalAliasEvent(newAlias)); + connection()->callApi( + id(), RoomCanonicalAliasEvent::matrixTypeId(), + QJsonObject { { "alias", newAlias }, + { "alt_aliases", QMatrixClient::toJson(altAliases()) } }); } void Room::setAliases(const QStringList& aliases) { - d->requestSetState(RoomAliasesEvent(aliases)); + connection()->callApi( + id(), RoomCanonicalAliasEvent::matrixTypeId(), + QJsonObject { { "alias", canonicalAlias() }, + { "alt_aliases", QMatrixClient::toJson(aliases) } }); } void Room::setTopic(const QString& newTopic) @@ -2183,15 +2199,31 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) , [] (const RoomNameEvent&) { return NameChange; } - , [this,oldStateEvent] (const RoomAliasesEvent& ae) { - const auto previousAliases = oldStateEvent - ? static_cast(oldStateEvent)->aliases() - : QStringList(); - connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); - return OtherChange; + , [] (const RoomAliasesEvent&) { + // This event has been removed by MSC-2432 + return NoChange; } - , [this] (const RoomCanonicalAliasEvent& evt) { + , [this, oldStateEvent] (const RoomCanonicalAliasEvent& evt) { setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); + + auto prevAliases = oldStateEvent ? fromJson( + oldStateEvent->contentJson()["alt_aliases"]) + : QStringList(); + if (oldStateEvent) { + const auto prevCanonicalAlias = + static_cast(oldStateEvent) + ->alias(); + if (!prevCanonicalAlias.isEmpty()) + prevAliases.push_back(prevCanonicalAlias); + } + + auto newAliases = + fromJson(evt.contentJson()["alt_aliases"]); + if (!evt.alias().isEmpty()) + newAliases.push_back(evt.alias()); + + connection()->updateRoomAliases(id(), prevAliases, newAliases); + return CanonicalAliasChange; } , [] (const RoomTopicEvent&) { diff --git a/lib/room.h b/lib/room.h index 33d1f4ea..9fd850e2 100644 --- a/lib/room.h +++ b/lib/room.h @@ -154,6 +154,7 @@ namespace QMatrixClient QString successorId() const; QString name() const; QStringList aliases() const; + QStringList altAliases() const; QString canonicalAlias() const; QString displayName() const; QString topic() const; -- cgit v1.2.3 From fb63012ec32c98c663802951153207442d1d63df Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 13 May 2019 20:42:50 +0900 Subject: linkifyUrls(): be more conservative in parsing serverparts Closes #321. --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 17674b84..914358d5 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -50,7 +50,7 @@ static void linkifyUrls(QString& htmlEscapedText) // An interim liberal implementation of // https://matrix.org/docs/spec/appendices.html#identifier-grammar static const QRegularExpression MxIdRegExp(QStringLiteral( - R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:[-.a-z0-9]+))" + R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))" ), RegExpOptions); // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," -- cgit v1.2.3 From dc53839b2afd2e1ee5af882924a1c0a5b553da4e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 08:56:01 +0100 Subject: Support for server notices rooms (MSC1452) Fixes #326. --- lib/events/accountdataevents.h | 1 + lib/jobs/basejob.cpp | 6 +++++- lib/room.cpp | 5 +++++ lib/room.h | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index a99d85ac..a43e358c 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -28,6 +28,7 @@ namespace QMatrixClient { constexpr const char* FavouriteTag = "m.favourite"; constexpr const char* LowPriorityTag = "m.lowpriority"; + constexpr const char* ServerNoticeTag = "m.server_notice"; struct TagRecord { diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 0d9b9f10..5af37902 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -333,7 +333,11 @@ void BaseJob::gotReply() d->status.message = tr("Requested room version: %1") .arg(json.value("room_version").toString()); - } else if (!json.isEmpty()) // Not localisable on the client side + } + else if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") + setStatus(IncorrectRequestError, + tr("It's not allowed to leave a server notices room")); + else if (!json.isEmpty()) // Not localisable on the client side setStatus(d->status.code, json.value("error"_ls).toString()); } } diff --git a/lib/room.cpp b/lib/room.cpp index 0942d730..77fbc2a5 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -971,6 +971,11 @@ bool Room::isLowPriority() const return d->tags.contains(LowPriorityTag); } +bool Room::isServerNoticeRoom() const +{ + return d->tags.contains(ServerNoticeTag); +} + bool Room::isDirectChat() const { return connection()->isDirectChat(id()); diff --git a/lib/room.h b/lib/room.h index 9fd850e2..b5753c79 100644 --- a/lib/room.h +++ b/lib/room.h @@ -348,6 +348,8 @@ namespace QMatrixClient bool isFavourite() const; /// Check whether the list of tags has m.lowpriority bool isLowPriority() const; + /// Check whether this room is for server notices (MSC1452) + bool isServerNoticeRoom() const; /// Check whether this room is a direct chat Q_INVOKABLE bool isDirectChat() const; -- cgit v1.2.3 From b4748f1a31808d21aeb359b21aedd3b483b535d1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 10:22:26 +0100 Subject: .well-known support Squashed cherry-pick of #330. --- lib/connection.cpp | 61 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index ac69228b..ddeaa883 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -32,12 +32,13 @@ #include "csapi/joining.h" #include "csapi/to_device.h" #include "csapi/room_send.h" +#include "csapi/wellknown.h" +#include "csapi/versions.h" #include "jobs/syncjob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" #include "csapi/voip.h" -#include #include #include #include @@ -177,37 +178,45 @@ void Connection::resolveServer(const QString& mxidOrDomain) } setHomeserver(maybeBaseUrl); - emit resolved(); - return; - // FIXME, #178: The below code is incorrect and is no more executed. The - // correct server resolution should be done from .well-known/matrix/client auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; - // Check if the Matrix server has a dedicated service record. - auto* dns = new QDnsLookup(); - dns->setType(QDnsLookup::SRV); - dns->setName("_matrix._tcp." + domain); - - connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() { - QUrl baseUrl { maybeBaseUrl }; - if (dns->error() == QDnsLookup::NoError && - dns->serviceRecords().isEmpty()) - { - auto record = dns->serviceRecords().front(); - baseUrl.setHost(record.target()); - baseUrl.setPort(record.port()); - qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host() - << "is" << baseUrl.authority(); + + auto getWellKnownJob = callApi(); + connect(getWellKnownJob, &BaseJob::finished, [this, getWellKnownJob, maybeBaseUrl] { + if (getWellKnownJob->status() == BaseJob::NotFoundError) { + qCDebug(MAIN) << "No .well-known file, IGNORE"; + } else if (getWellKnownJob->status() != BaseJob::Success) { + qCDebug(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; + emit resolveError(tr("Fetching .well-known file failed")); + return; + } else if (getWellKnownJob->data().homeserver.baseUrl.isEmpty()) { + qCDebug(MAIN) << "base_url not provided, FAIL_PROMPT"; + emit resolveError(tr("base_url not provided")); + return; + } else if (!QUrl(getWellKnownJob->data().homeserver.baseUrl).isValid()) { + qCDebug(MAIN) << "base_url invalid, FAIL_ERROR"; + emit resolveError(tr("base_url invalid")); + return; } else { - qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record" - << dns->name() << "- using the hostname as is"; + QUrl baseUrl(getWellKnownJob->data().homeserver.baseUrl); + + qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.authority(); + setHomeserver(baseUrl); } - setHomeserver(baseUrl); - emit resolved(); - dns->deleteLater(); + + auto getVersionsJob = callApi(); + + connect(getVersionsJob, &BaseJob::finished, [this, getVersionsJob] { + if (getVersionsJob->status() == BaseJob::Success) { + qCDebug(MAIN) << "homeserver url is valid"; + emit resolved(); + } else { + qCDebug(MAIN) << "homeserver url invalid"; + emit resolveError(tr("homeserver url invalid")); + } + }); }); - dns->lookup(); } void Connection::connectToServer(const QString& user, const QString& password, -- cgit v1.2.3 From 10b5ab9e13c46f275c3b1e567fb6a6421b103e6e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 25 Aug 2019 18:56:03 +0900 Subject: setAvatarForRoom: try recover from otherAvatars inconsistency Cherry-pick of what seems to be a fix for #347. --- lib/user.cpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/user.cpp b/lib/user.cpp index 17db5760..535551cf 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -165,19 +165,26 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, if (newUrl != mostUsedAvatar.url()) { // Check if the new avatar is about to become most used. - if (avatarsToRooms.count(newUrl) >= totalRooms - avatarsToRooms.size()) - { + const auto newUrlUsage = avatarsToRooms.count(newUrl); + if (newUrlUsage >= totalRooms - avatarsToRooms.size()) { QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used avatar of user" << userId - << "from" << mostUsedAvatar.url().toDisplayString() - << "to" << newUrl.toDisplayString(); + if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) { + qCInfo(MAIN) << "Switching the most used avatar of user" << userId + << "from" << mostUsedAvatar.url().toDisplayString() + << "to" << newUrl.toDisplayString(); et.start(); } avatarsToRooms.remove(newUrl); auto nextMostUsedIt = otherAvatar(newUrl); - Q_ASSERT(nextMostUsedIt != otherAvatars.end()); + if (nextMostUsedIt == otherAvatars.end()) { + qCCritical(MAIN) + << userId << "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); const auto& roomMap = connection->roomMap(); for (const auto* r1: roomMap) -- cgit v1.2.3 From 94581d2a55853e91e117301b408446d9426ef8e6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 10:37:00 +0100 Subject: Update the cache for sure if unread counters were changed Backport of #345. --- lib/room.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 77fbc2a5..43abc21d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1362,17 +1362,20 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) { qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; + roomChanges |= Change::UnreadNotifsChange; emit unreadMessagesChanged(this); } if( data.highlightCount != d->highlightCount ) { d->highlightCount = data.highlightCount; + roomChanges |= Change::UnreadNotifsChange; emit highlightCountChanged(this); } if( data.notificationCount != d->notificationCount ) { d->notificationCount = data.notificationCount; + roomChanges |= Change::UnreadNotifsChange; emit notificationCountChanged(this); } if (roomChanges != Change::NoChange) -- cgit v1.2.3 From f7442619401fa789647101f56fdfbddaf4fc6796 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 10:40:19 +0100 Subject: BaseJob: support M_USER_DEACTIVATED error code Backport for #344. --- lib/jobs/basejob.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 5af37902..a6225c67 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -337,6 +337,9 @@ void BaseJob::gotReply() else if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") setStatus(IncorrectRequestError, tr("It's not allowed to leave a server notices room")); + else if (errCode == "M_USER_DEACTIVATED") + setStatus(ContentAccessError, + tr("The user has been deactivated")); else if (!json.isEmpty()) // Not localisable on the client side setStatus(d->status.code, json.value("error"_ls).toString()); } -- cgit v1.2.3 From 4c59f10262b3de2e8b808b0839d772b0d70d2057 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 16:55:03 +0100 Subject: qmc-example: make tests work again --- examples/qmc-example.cpp | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index bd9190b9..2aaaf147 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -183,7 +183,7 @@ void QMCTest::loadMembers() // The dedicated qmc-test room is too small to test // lazy-loading-then-full-loading; use #qmatrixclient:matrix.org instead. // TODO: #264 - auto* r = c->room(QStringLiteral("!PCzUtxtOjUySxSelof:matrix.org")); + auto* r = c->roomByAlias(QStringLiteral("#qmatrixclient:matrix.org")); if (!r) { cout << "#test:matrix.org is not found in the test user's rooms" << endl; @@ -344,8 +344,9 @@ void QMCTest::setTopic() const auto newTopic = c->generateTxnId(); targetRoom->setTopic(newTopic); // Sets the state by proper means const auto fakeTopic = c->generateTxnId(); - targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event - RoomTopicEvent(fakeTopic).contentJson()); + auto fakeTxnId = + targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event + RoomTopicEvent(fakeTopic).contentJson()); connectUntil(targetRoom, &Room::topicChanged, this, [this,newTopic,fakeTopic,initialTopic] { @@ -353,22 +354,25 @@ void QMCTest::setTopic() { QMC_CHECK(stateTestName, true); // Don't reset the topic yet if the negative test still runs - if (!running.contains(fakeStateTestName)) - targetRoom->setTopic(initialTopic); + targetRoom->setTopic(initialTopic); return true; } return false; }); - connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, - [this,fakeTopic,initialTopic] (const RoomEvent* e, int) { - if (e->contentJson().value("topic").toString() != fakeTopic) - return false; // Wait on for the right event + connectUntil(targetRoom, &Room::pendingEventChanged, this, + [this, fakeTxnId](int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); - QMC_CHECK(fakeStateTestName, !e->isStateEvent()); - if (!running.contains(fakeStateTestName)) - targetRoom->setTopic(initialTopic); + const auto& pendingItem = pendingEvents[pendingIdx]; + if (pendingItem->transactionId() != fakeTxnId + || pendingItem.deliveryStatus() <= EventStatus::Departed) + return false; + + QMC_CHECK(fakeStateTestName, pendingItem.deliveryStatus() + == EventStatus::SendingFailed); return true; }); } -- cgit v1.2.3 From 76265ca6aa7a6c8d777aa55190d9038600ef11b1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 23:03:09 +0100 Subject: BaseJob: Only send access token when needed This is a basic fix for #358, without a workaround added in 5937127b (such workaround would break API compatibility). --- lib/jobs/basejob.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a6225c67..a1f49b63 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -197,8 +197,9 @@ void BaseJob::Private::sendRequest(bool inBackground) { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) }; if (!requestHeaders.contains("Content-Type")) req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - req.setRawHeader("Authorization", - QByteArray("Bearer ") + connection->accessToken()); + if (needsToken) + req.setRawHeader("Authorization", + QByteArray("Bearer ") + connection->accessToken()); req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -- cgit v1.2.3 From 27a2f59722b6e6642dae05cd4f04f5f40304eeb1 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Sat, 30 Nov 2019 03:13:23 +0300 Subject: Fix room highlighting for names with hashtag Fixes #359 --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 914358d5..81862ab6 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -50,7 +50,7 @@ static void linkifyUrls(QString& htmlEscapedText) // An interim liberal implementation of // 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"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))" ), RegExpOptions); // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," -- cgit v1.2.3 From fe1a7c61a81ff4820645ecafccd693ea0a887d36 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 9 Dec 2019 11:52:26 +0300 Subject: Initialise read marker if none is found in the whole timeline Backport of a fix to #361. --- lib/room.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 43abc21d..022a3bed 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -183,6 +183,7 @@ class Room::Private rev_iter_t timelineBase() const { return q->findInTimeline(-1); } void getPreviousContent(int limit = 10); + bool allHistoryLoaded() const; template const EventT* getCurrentState(const QString& stateKey = {}) const @@ -370,6 +371,11 @@ const Room::PendingEvents& Room::pendingEvents() const return d->unsyncedEvents; } +bool Room::Private::allHistoryLoaded() const +{ + return !timeline.empty() && is(*timeline.front()); +} + QString Room::name() const { return d->getCurrentState()->name(); @@ -503,7 +509,9 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) // that has just arrived. In this case we should recalculate // unreadMessages and might need to promote the read marker further // over local-origin messages. - const auto readMarker = q->readMarker(); + auto readMarker = q->readMarker(); + if (readMarker == timeline.crend() && allHistoryLoaded()) + --readMarker; // Read marker not found in the timeline, initialise it if (readMarker >= from && readMarker < to) { promoteReadMarker(q->localUser(), readMarker, true); -- cgit v1.2.3 From 24be625ff62601ac8c61a4269b5fbe7d8405dae7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 24 Mar 2020 07:53:31 +0100 Subject: Redaction: update essential keys list No more special: - prev_content (MSC1954, fixes #318) - aliases (follow up on MSC2432) --- lib/room.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 022a3bed..3d16f285 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1925,7 +1925,6 @@ RoomEventPtr makeRedacted(const RoomEvent& target, static const QStringList keepKeys { EventIdKey, TypeKey, QStringLiteral("room_id"), QStringLiteral("sender"), QStringLiteral("state_key"), - QStringLiteral("prev_content"), ContentKey, QStringLiteral("hashes"), QStringLiteral("signatures"), QStringLiteral("depth"), QStringLiteral("prev_events"), QStringLiteral("prev_state"), QStringLiteral("auth_events"), @@ -1942,7 +1941,6 @@ RoomEventPtr makeRedacted(const RoomEvent& target, // QStringLiteral("events_default"), QStringLiteral("kick"), // QStringLiteral("redact"), QStringLiteral("state_default"), // QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } // , { RoomHistoryVisibility::typeId(), // { QStringLiteral("history_visibility") } } }; -- cgit v1.2.3 From df310485be69a5891b2dc57391854951709d474e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 13 Dec 2019 14:47:26 +0300 Subject: Room: make downloaded file name building more robust Backport of a fix for #366. --- lib/room.cpp | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 3d16f285..960d8147 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -994,6 +994,11 @@ QList Room::directChatUsers() const return connection()->directChatUsers(this); } +QString safeFileName(QString rawName) +{ + return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_"); +} + const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { @@ -1010,24 +1015,26 @@ Room::Private::getEventWithFile(const QString& eventId) const QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const { - Q_ASSERT(event->hasFileContent()); + Q_ASSERT(event && event->hasFileContent()); const auto* fileInfo = event->content()->fileInfo(); QString fileName; if (!fileInfo->originalName.isEmpty()) - { - fileName = QFileInfo(fileInfo->originalName).fileName(); - } - else if (!event->plainBody().isEmpty()) - { + fileName = QFileInfo(safeFileName(fileInfo->originalName)).fileName(); + else { // Having no better options, assume that the body has // the original file URL or at least the file name. QUrl u { event->plainBody() }; if (u.isValid()) - fileName = QFileInfo(u.path()).fileName(); + { + qDebug(MAIN) << event->id() + << "has no file name supplied but the event body " + "looks like a URL - using the file name from it"; + fileName = u.fileName(); + } } - // Check the file name for sanity - if (fileName.isEmpty() || !QTemporaryFile(fileName).open()) - return "file." % fileInfo->mimeType.preferredSuffix(); + if (fileName.isEmpty()) + return safeFileName(fileInfo->mediaId()).replace('.', '-') % '.' + % fileInfo->mimeType.preferredSuffix(); if (QSysInfo::productType() == "windows") { @@ -1845,17 +1852,20 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) if (filePath.isEmpty()) { // Build our own file path, starting with temp directory and eventId. - filePath = eventId; - filePath = QDir::tempPath() % '/' % - filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") % - '#' % d->fileNameToDownload(event); + filePath = + 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, "---"); + + filePath = QDir::tempPath() % '/' % filePath; + qDebug(MAIN) << "File path:" << filePath; } auto job = connection()->downloadFile(fileUrl, filePath); if (isJobRunning(job)) { - // If there was a previous transfer (completed or failed), remove it. - d->fileTransfers.remove(eventId); - d->fileTransfers.insert(eventId, { job, job->targetFileName() }); + // 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); -- cgit v1.2.3 From 5b7032e414899b5f9e8f19aec567eaed5e0fc4c2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 24 Mar 2020 22:30:53 +0100 Subject: Support of MSC1849 message editing This is a backport of #341, as fixed by #373 and #376. --- lib/events/roomevent.cpp | 14 ++++++ lib/events/roomevent.h | 2 + lib/events/roommessageevent.cpp | 108 ++++++++++++++++++++++++++++++---------- lib/events/roommessageevent.h | 3 ++ lib/room.cpp | 93 +++++++++++++++++++++++++++++++--- 5 files changed, 187 insertions(+), 33 deletions(-) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3d03509f..5e2d0b3c 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -66,6 +66,20 @@ QString RoomEvent::senderId() const return fullJson()["sender"_ls].toString(); } +bool RoomEvent::isReplaced() const +{ + return unsignedJson()["m.relations"_ls].toObject().contains("m.replace"); +} + +QString RoomEvent::replacedBy() const +{ + // clang-format off + return unsignedJson()["m.relations"_ls].toObject() + .value("m.replace").toObject() + .value(EventIdKeyL).toString(); + // clang-format on +} + QString RoomEvent::redactionReason() const { return isRedacted() ? _redactedBecause->reason() : QString{}; diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index ce96174e..8926ab0f 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -51,6 +51,8 @@ namespace QMatrixClient QDateTime timestamp() const; QString roomId() const; QString senderId() const; + bool isReplaced() const; + QString replacedBy() const; bool isRedacted() const { return bool(_redactedBecause); } const event_ptr_tt& redactedBecause() const { diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 8f4e0ebc..1edf82e4 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -30,12 +30,13 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; -static const auto RelatesToKey = "m.relates_to"_ls; -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; -static const auto FormattedBodyKey = "formatted_body"_ls; +static const auto RelatesToKeyL = "m.relates_to"_ls; +static const auto MsgTypeKeyL = "msgtype"_ls; +static const auto BodyKeyL = "body"_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"); @@ -49,7 +50,7 @@ TypedBase* make(const QJsonObject& json) template <> TypedBase* make(const QJsonObject& json) { - return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) ? new TextContent(json) : nullptr; } @@ -62,7 +63,7 @@ struct MsgTypeDesc const std::vector msgTypes = { { TextTypeKey, MsgType::Text, make } - , { QStringLiteral("m.emote"), MsgType::Emote, make } + , { EmoteTypeKey, MsgType::Emote, make } , { NoticeTypeKey, MsgType::Notice, make } , { QStringLiteral("m.image"), MsgType::Image, make } , { QStringLiteral("m.file"), MsgType::File, make } @@ -95,12 +96,27 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); - if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && - json.contains(RelatesToKey)) - { - json.remove(RelatesToKey); - qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType - << "messages; the relation has been stripped off"; + if (json.contains(RelatesToKeyL)) { + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey + && jsonMsgType != EmoteTypeKey) { + json.remove(RelatesToKeyL); + qCWarning(EVENTS) + << RelatesToKeyL << "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 RelatesTo structure is not omitted + auto* textContent = static_cast(content); + if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKeyL, plainBody); + newContentJson.insert(MsgTypeKeyL, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[MsgTypeKeyL] = jsonMsgType; + json[BodyKeyL] = "* " + plainBody; + return json; + } + } } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); @@ -159,9 +175,9 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if (isRedacted()) return; const QJsonObject content = contentJson(); - if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) + if ( content.contains(MsgTypeKeyL) && content.contains(BodyKeyL) ) { - auto msgtype = content[MsgTypeKey].toString(); + auto msgtype = content[MsgTypeKeyL].toString(); bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) @@ -191,12 +207,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentJson()[MsgTypeKey].toString(); + return contentJson()[MsgTypeKeyL].toString(); } QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKey].toString(); + return contentJson()[BodyKeyL].toString(); } QMimeType RoomMessageEvent::mimeType() const @@ -223,6 +239,16 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } +QString RoomMessageEvent::replacedEvent() const +{ + if (!content() || !hasTextContent()) + return {}; + + const auto& rel = static_cast(content())->relatesTo; + return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId() + ? rel->eventId : QString(); +} + QString rawMsgTypeForMimeType(const QMimeType& mimeType) { auto name = mimeType.name(); @@ -251,41 +277,69 @@ TextContent::TextContent(const QString& text, const QString& contentType, mimeType = QMimeDatabase().mimeTypeForName("text/html"); } +namespace QMatrixClient +{ +Omittable relationFromJson(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() }; +} +} + TextContent::TextContent(const QJsonObject& json) + : relatesTo(relationFromJson(json[RelatesToKeyL])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); static const auto HtmlMimeType = db.mimeTypeForName("text/html"); + const auto actualJson = + relatesTo.omitted() || relatesTo->type != RelatesTo::ReplacementTypeId() + ? json : json.value("m.new_content"_ls).toObject(); // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == HtmlContentTypeId) + if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json[FormattedBodyKey].toString(); + body = actualJson[FormattedBodyKeyL].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; - body = json[BodyKey].toString(); + body = actualJson[BodyKeyL].toString(); } - const auto replyJson = json[RelatesToKey].toObject() - .value(RelatesTo::ReplyTypeId()).toObject(); - if (!replyJson.isEmpty()) - relatesTo = replyTo(fromJson(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const { + static const auto FormatKey = QStringLiteral("format"); + static const auto RichBodyKey = QStringLiteral("formatted_body"); + Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), HtmlContentTypeId); - json->insert(QStringLiteral("formatted_body"), body); + json->insert(FormatKey, HtmlContentTypeId); + json->insert(RichBodyKey, body); } - if (!relatesTo.omitted()) + if (!relatesTo.omitted()) { json->insert(QStringLiteral("m.relates_to"), - QJsonObject { { relatesTo->type, relatesTo->eventId } }); + QJsonObject { { relatesTo->type, relatesTo->eventId } }); + if (relatesTo->type == RelatesTo::ReplacementTypeId()) { + QJsonObject newContentJson; + if (mimeType.inherits("text/html")) { + json->insert(FormatKey, HtmlContentTypeId); + json->insert(RichBodyKey, body); + } + json->insert(QStringLiteral("m.new_content"), newContentJson); + } + } } LocationContent::LocationContent(const QString& geoUri, diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index c2e075eb..7320e4ea 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -72,6 +72,7 @@ namespace QMatrixClient bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; + QString replacedEvent() const; static QString rawMsgTypeForUrl(const QUrl& url); static QString rawMsgTypeForFile(const QFileInfo& fi); @@ -79,6 +80,7 @@ namespace QMatrixClient private: QScopedPointer _content; + // FIXME: should it really be static? static QJsonObject assembleContentJson(const QString& plainBody, const QString& jsonMsgType, EventContent::TypedBase* content); @@ -95,6 +97,7 @@ namespace QMatrixClient 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; }; diff --git a/lib/room.cpp b/lib/room.cpp index 960d8147..ec000519 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -293,9 +293,18 @@ class Room::Private * * Tries to find an event in the timeline and redact it; deletes the * redaction event whether the redacted event was found or not. + * \return true if the event has been found and redacted; false otherwise */ bool processRedaction(const RedactionEvent& redaction); + /*! Apply a new revision of the event to the timeline + * + * Tries to find an event in the timeline and replace it with the new + * content passed in \p newMessage. + * \return true if the event has been found and replaced; false otherwise + */ + bool processReplacement(const RoomMessageEvent& newMessage); + void setTags(TagsMap newTags); QJsonObject toJson() const; @@ -2028,6 +2037,52 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) return true; } +/** Make a replaced event + * + * Takes \p target and returns a copy of it with content taken from + * \p replacement. Disposal of the original event after that is on the caller. + */ +RoomEventPtr makeReplaced(const RoomEvent& target, + const RoomMessageEvent& replacement) +{ + auto originalJson = target.originalJsonObject(); + originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls); + + auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); + auto relations = unsignedData.take("m.relations"_ls).toObject(); + relations["m.replace"_ls] = replacement.id(); + unsignedData.insert(QStringLiteral("m.relations"), relations); + originalJson.insert(UnsignedKey, unsignedData); + + return loadEvent(originalJson); +} + +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()) + return false; + + Q_ASSERT(q->isValidIndex(*pIdx)); + + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + if (ti->replacedBy() == newEvent.id()) + { + qCDebug(MAIN) << "Event" << ti->id() << "is already replaced with" + << newEvent.id(); + return true; + } + + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. + auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent)); + qCDebug(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); + emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + return true; +} + Connection* Room::connection() const { Q_ASSERT(d->connection); @@ -2039,10 +2094,16 @@ User* Room::localUser() const return connection()->user(); } -inline bool isRedaction(const RoomEventPtr& ep) +/// Whether the event is a redaction or a replacement +inline bool isEditing(const RoomEventPtr& ep) { Q_ASSERT(ep); - return is(*ep); + if (is(*ep)) + return true; + if (auto* msgEvent = eventCast(ep)) + return msgEvent->replacedEvent().isEmpty(); + + return false; } Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) @@ -2054,18 +2115,19 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. // NB: We have to store redaction events to the timeline too - see #220. - auto redactionIt = std::find_if(events.begin(), events.end(), isRedaction); - for(const auto& eptr: RoomEventsRange(redactionIt, events.end())) + auto it = std::find_if(events.begin(), events.end(), isEditing); + for(const auto& eptr: RoomEventsRange(it, events.end())) + { if (auto* r = eventCast(eptr)) { // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; - auto targetIt = std::find_if(events.begin(), redactionIt, + auto targetIt = std::find_if(events.begin(), it, [id=r->redactedEvent()] (const RoomEventPtr& ep) { return ep->id() == id; }); - if (targetIt != redactionIt) + if (targetIt != it) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(MAIN) << "Redaction" << r->id() @@ -2073,6 +2135,25 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << "is not found"; // If the target event comes later, it comes already redacted. } + if (auto* msg = eventCast(eptr)) { + if (!msg->replacedEvent().isEmpty()) { + if (processReplacement(*msg)) + continue; + auto targetIt = std::find_if(events.begin(), it, + [id=msg->replacedEvent()] (const RoomEventPtr& ep) { + return ep->id() == id; + }); + if (targetIt != it) + *targetIt = makeReplaced(**targetIt, *msg); + else // FIXME: don't ignore, just show it wherever it arrived + qCDebug(MAIN) << "Replacing event" << msg->id() + << "ignored: replaced event" << msg->replacedEvent() + << "is not found"; + // Same as with redactions above, the replaced event coming + // later will come already with the new content. + } + } + } // State changes arrive as a part of timeline; the current room state gets // updated before merging events to the timeline because that's what -- cgit v1.2.3 From 16a0a88b3db9e8c3f1c8ff80139b77a31f2da287 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 25 Mar 2020 14:34:31 +0100 Subject: Support for receiving m.reaction events Continuation of the #341 backport. --- CMakeLists.txt | 1 + examples/qmc-example.cpp | 46 +++++++++++++++- lib/events/reactionevent.cpp | 44 ++++++++++++++++ lib/events/reactionevent.h | 78 +++++++++++++++++++++++++++ lib/room.cpp | 123 ++++++++++++++++++++++++++++++------------- lib/room.h | 10 ++++ libqmatrixclient.pri | 2 + 7 files changed, 266 insertions(+), 38 deletions(-) create mode 100644 lib/events/reactionevent.cpp create mode 100644 lib/events/reactionevent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b2536f1b..8ae97a6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ set(libqmatrixclient_SRCS lib/events/roommemberevent.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 diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 2aaaf147..64514619 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -6,6 +6,7 @@ #include "csapi/joining.h" #include "csapi/leaving.h" #include "events/simplestateevents.h" +#include "events/reactionevent.h" #include #include @@ -26,12 +27,14 @@ class QMCTest : public QObject QMCTest(Connection* conn, QString testRoomName, QString source); private slots: + // clang-format off void setupAndRun(); void onNewRoom(Room* r); void run(); void doTests(); void loadMembers(); void sendMessage(); + void sendReaction(const QString& targetEvtId); void sendFile(); void checkFileSendingOutcome(const QString& txnId, const QString& fileName); @@ -44,6 +47,7 @@ class QMCTest : public QObject const Connection::DirectChatsMap& added); void conclude(); void finalize(); + // clang-format on private: QScopedPointer c; @@ -230,8 +234,48 @@ void QMCTest::sendMessage() is(*evt) && !evt->id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() == evt->transactionId()); + sendReaction(evt->id()); return true; - }); + }); +} + +void QMCTest::sendReaction(const QString& targetEvtId) +{ + running.push_back("Reaction sending"); + cout << "Reacting to the newest message in the room" << endl; + Q_ASSERT(targetRoom->timelineSize() > 0); + const auto key = QStringLiteral("+1"); + auto txnId = targetRoom->postReaction(targetEvtId, key); + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; + QMC_CHECK("Reaction sending", false); + return; + } + + // TODO: Check that it came back as a reaction event and that it attached to + // the right event + connectUntil(targetRoom, &Room::updatedEvent, this, + [this, 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) { + QMC_CHECK("Reaction sending", false); + } else { + const auto* evt = + eventCast(reactions.back()); + QMC_CHECK("Reaction sending", + is(*evt) + && !evt->id().isEmpty() + && evt->relation().key == key + && evt->transactionId() == txnId); + } + return true; + }); } void QMCTest::sendFile() diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp new file mode 100644 index 00000000..0081edc2 --- /dev/null +++ b/lib/events/reactionevent.cpp @@ -0,0 +1,44 @@ +/****************************************************************************** + * Copyright (C) 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 + */ + +#include "reactionevent.h" + +using namespace QMatrixClient; + +void QMatrixClient::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 QMatrixClient::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 new file mode 100644 index 00000000..a422abeb --- /dev/null +++ b/lib/events/reactionevent.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * Copyright (C) 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 + */ + +#pragma once + +#include "roomevent.h" + +namespace QMatrixClient { + +struct 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 EventRelation { Reply(), std::move(eventId) }; + } + static EventRelation annotate(QString eventId, QString key) + { + return EventRelation { Annotation(), std::move(eventId), std::move(key) }; + } + static EventRelation replace(QString eventId) + { + return EventRelation { Replacement(), std::move(eventId) }; + } +}; +template <> +struct JsonObjectConverter +{ + static void dumpTo(QJsonObject& jo, const EventRelation& pod); + static void fillFrom(const QJsonObject& jo, EventRelation& pod); +}; + +class 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 content(QStringLiteral("m.relates_to")); + } + +private: + EventRelation _relation; +}; +REGISTER_EVENT_TYPE(ReactionEvent) + +} // namespace QMatrixClient diff --git a/lib/room.cpp b/lib/room.cpp index ec000519..3cabe948 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -37,6 +37,7 @@ #include "events/roommemberevent.h" #include "events/typingevent.h" #include "events/receiptevent.h" +#include "events/reactionevent.h" #include "events/callinviteevent.h" #include "events/callcandidatesevent.h" #include "events/callanswerevent.h" @@ -98,6 +99,10 @@ class Room::Private Timeline timeline; PendingEvents unsyncedEvents; QHash eventsIndex; + // 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; QString displayname; Avatar avatar; int highlightCount = 0; @@ -707,10 +712,10 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) { auto it = findInTimeline(d->eventsIndex.value(evtId)); - Q_ASSERT((*it)->id() == evtId); + Q_ASSERT(it != historyEdge() && (*it)->id() == evtId); return it; } - return timelineEdge(); + return historyEdge(); } Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) @@ -726,6 +731,18 @@ Room::findPendingEvent(const QString& txnId) const [txnId] (const auto& item) { return item->transactionId() == txnId; }); } +const Room::RelatedEvents Room::relatedEvents(const QString& evtId, + const char* relType) const +{ + return d->relations.value({ evtId, relType }); +} + +const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, + const char* relType) const +{ + return relatedEvents(evt.id(), relType); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -1569,6 +1586,11 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html); } +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) { @@ -2032,6 +2054,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) updateDisplayname(); } } + if (const auto* reaction = eventCast(oldEvent)) { + const auto& targetEvtId = reaction->relation().eventId; + const auto lookupKey = qMakePair(targetEvtId, + EventRelation::Annotation()); + if (relations.contains(lookupKey)) { + relations[lookupKey].removeOne(reaction); + } + } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; @@ -2112,45 +2142,49 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (events.empty()) return Change::NoChange; - // Pre-process redactions so that events that get redacted in the same - // batch landed in the timeline already redacted. - // NB: We have to store redaction events to the timeline too - see #220. - auto it = std::find_if(events.begin(), events.end(), isEditing); - for(const auto& eptr: RoomEventsRange(it, events.end())) { - if (auto* r = eventCast(eptr)) - { - // Try to find the target in the timeline, then in the batch. - if (processRedaction(*r)) - continue; - auto targetIt = std::find_if(events.begin(), it, - [id=r->redactedEvent()] (const RoomEventPtr& ep) { - return ep->id() == id; - }); - if (targetIt != it) - *targetIt = makeRedacted(**targetIt, *r); - else - qCDebug(MAIN) << "Redaction" << r->id() - << "ignored: target event" << r->redactedEvent() - << "is not found"; - // If the target event comes later, it comes already redacted. - } - if (auto* msg = eventCast(eptr)) { - if (!msg->replacedEvent().isEmpty()) { - if (processReplacement(*msg)) + // Pre-process redactions and edits so that events that get + // redacted/replaced in the same batch landed in the timeline already + // treated. + // NB: We have to store redacting/replacing events to the timeline too - + // see #220. + auto it = std::find_if(events.begin(), events.end(), isEditing); + for (const auto& eptr: RoomEventsRange(it, events.end())) { + if (auto* r = eventCast(eptr)) { + // Try to find the target in the timeline, then in the batch. + if (processRedaction(*r)) continue; auto targetIt = std::find_if(events.begin(), it, - [id=msg->replacedEvent()] (const RoomEventPtr& ep) { - return ep->id() == id; - }); + [id = r->redactedEvent()]( + const RoomEventPtr& ep) { + return ep->id() == id; + }); if (targetIt != it) - *targetIt = makeReplaced(**targetIt, *msg); - else // FIXME: don't ignore, just show it wherever it arrived - qCDebug(MAIN) << "Replacing event" << msg->id() - << "ignored: replaced event" << msg->replacedEvent() - << "is not found"; - // Same as with redactions above, the replaced event coming - // later will come already with the new content. + *targetIt = makeRedacted(**targetIt, *r); + else + qCDebug(MAIN) + << "Redaction" << r->id() << "ignored: target event" + << r->redactedEvent() << "is not found"; + // If the target event comes later, it comes already redacted. + } + if (auto* msg = eventCast(eptr)) { + if (!msg->replacedEvent().isEmpty()) { + if (processReplacement(*msg)) + continue; + auto targetIt = std::find_if(events.begin(), it, + [id = msg->replacedEvent()]( + const RoomEventPtr& ep) { + return ep->id() == id; + }); + if (targetIt != it) + *targetIt = makeReplaced(**targetIt, *msg); + else // FIXME: don't ignore, just show it wherever it arrived + qCDebug(MAIN) << "Replacing event" << msg->id() + << "ignored: replaced event" + << msg->replacedEvent() << "is not found"; + // Same as with redactions above, the replaced event coming + // later will come already with the new content. + } } } } @@ -2219,6 +2253,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (totalInserted > 0) { + for (auto it = from; it != timeline.cend(); ++it) { + if (const auto* reaction = it->viewAs()) { + const auto& relation = reaction->relation(); + relations[{ relation.eventId, relation.type }] << reaction; + emit q->updatedEvent(relation.eventId); + } + } + qCDebug(MAIN) << "Room" << q->objectName() << "received" << totalInserted << "new events; the last event is now" << timeline.back(); @@ -2277,6 +2319,13 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); + for (auto it = from; it != timeline.crend(); ++it) { + if (const auto* reaction = it->viewAs()) { + const auto& relation = reaction->relation(); + relations[{ relation.eventId, relation.type }] << reaction; + emit q->updatedEvent(relation.eventId); + } + } if (from <= q->readMarker()) updateUnreadCount(from, timeline.crend()); diff --git a/lib/room.h b/lib/room.h index b5753c79..87ff3b5d 100644 --- a/lib/room.h +++ b/lib/room.h @@ -117,6 +117,7 @@ namespace QMatrixClient public: using Timeline = std::deque; using PendingEvents = std::vector; + using RelatedEvents = QVector; using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; @@ -248,6 +249,11 @@ namespace QMatrixClient PendingEvents::iterator findPendingEvent(const QString & txnId); PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; + const RelatedEvents relatedEvents(const QString& evtId, + const char* relType) const; + const RelatedEvents relatedEvents(const RoomEvent& evt, + const char* relType) const; + bool displayed() const; /// Mark the room as currently displayed to the user /** @@ -413,6 +419,9 @@ namespace QMatrixClient const QString& html, MessageEventType type = MessageEventType::Text); 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, const QUrl& localPath, bool asGenericFile = false); /** Post a pre-created room message event @@ -559,6 +568,7 @@ namespace QMatrixClient void tagsAboutToChange(); void tagsChanged(); + void updatedEvent(QString eventId); void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent); diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index be568bd2..79f1d50b 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -32,6 +32,7 @@ HEADERS += \ $$SRCPATH/events/roomavatarevent.h \ $$SRCPATH/events/typingevent.h \ $$SRCPATH/events/receiptevent.h \ + $$SRCPATH/events/reactionevent.h \ $$SRCPATH/events/callanswerevent.h \ $$SRCPATH/events/callcandidatesevent.h \ $$SRCPATH/events/callhangupevent.h \ @@ -75,6 +76,7 @@ SOURCES += \ $$SRCPATH/events/roommessageevent.cpp \ $$SRCPATH/events/roommemberevent.cpp \ $$SRCPATH/events/typingevent.cpp \ + $$SRCPATH/events/reactionevent.cpp \ $$SRCPATH/events/callanswerevent.cpp \ $$SRCPATH/events/callcandidatesevent.cpp \ $$SRCPATH/events/callhangupevent.cpp \ -- cgit v1.2.3 From 7033aa5ad8152f77164a4c644837d1fe44aa0430 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 25 Mar 2020 20:35:30 +0100 Subject: Connection: support getting the list of login flows Backport of #386. --- lib/connection.cpp | 43 +++++++++++++++++++++++++++++++++++-------- lib/connection.h | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index ddeaa883..99d5c04a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -80,10 +80,10 @@ class Connection::Private std::unique_ptr data; // A complex key below is a pair of room name and whether its // state is Invited. The spec mandates to keep Invited room state - // separately so we should, e.g., keep objects for Invite and - // Leave state of the same room. + // 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; - // Mapping from aliases to room ids, as per the last sync + /// Mapping from aliases to room ids, as of the last sync QHash roomAliasMap; QVector roomIdsToForget; QVector firstTimeRooms; @@ -102,6 +102,8 @@ class Connection::Private GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; + QVector loginFlows; + SyncJob* syncJob = nullptr; bool cacheState = true; @@ -109,7 +111,7 @@ class Connection::Private .value("cache_type").toString() != "json"; bool lazyLoading = false; - void connectWithToken(const QString& user, const QString& accessToken, + void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); template @@ -859,6 +861,21 @@ QString Connection::domain() const return d->userId.section(':', 1); } +QVector Connection::loginFlows() const +{ + return d->loginFlows; +} + +bool Connection::supportsPasswordAuth() const +{ + return d->loginFlows.contains(LoginFlows::Password); +} + +bool Connection::supportsSso() const +{ + return d->loginFlows.contains(LoginFlows::SSO); +} + Room* Connection::room(const QString& roomId, JoinStates states) const { Room* room = d->roomMap.value({roomId, false}, nullptr); @@ -1253,11 +1270,21 @@ QByteArray Connection::generateTxnId() const void Connection::setHomeserver(const QUrl& url) { - if (homeserver() == url) - return; + if (homeserver() != url) { + d->data->setBaseUrl(url); + d->loginFlows.clear(); + emit homeserverChanged(homeserver()); + } - d->data->setBaseUrl(url); - emit homeserverChanged(homeserver()); + // Whenever a homeserver is updated, retrieve available login flows from it + auto* j = callApi(BackgroundRequest); + connect(j, &BaseJob::finished, this, [this, j] { + if (j->status().good()) + d->loginFlows = j->flows(); + else + d->loginFlows.clear(); + emit loginFlowsChanged(); + }); } void Connection::saveRoomState(Room* r) const diff --git a/lib/connection.h b/lib/connection.h index ea5be51a..ad228bf0 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -18,6 +18,7 @@ #pragma once +#include "csapi/login.h" #include "csapi/create_room.h" #include "joinstate.h" #include "events/accountdataevents.h" @@ -30,6 +31,8 @@ #include #include +Q_DECLARE_METATYPE(QMatrixClient::GetLoginFlowsJob::LoginFlow) + namespace QMatrixClient { class Room; @@ -51,6 +54,28 @@ namespace QMatrixClient class SendMessageJob; class LeaveRoomJob; + // To simplify comparisons of LoginFlows + + inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs, + const GetLoginFlowsJob::LoginFlow& rhs) + { + return lhs.type == rhs.type; + } + + inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs, + const GetLoginFlowsJob::LoginFlow& rhs) + { + return !(lhs == rhs); + } + + /// Predefined login flows + namespace LoginFlows { + using LoginFlow = GetLoginFlowsJob::LoginFlow; + static const LoginFlow Password { "m.login.password" }; + static const LoginFlow SSO { "m.login.sso" }; + static const LoginFlow Token { "m.login.token" }; + } + class Connection; using room_factory_t = std::function loginFlows READ loginFlows NOTIFY loginFlowsChanged) + Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged) + Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) @@ -246,6 +271,12 @@ namespace QMatrixClient QUrl homeserver() const; /** Get the domain name used for ids/aliases on the server */ QString domain() const; + /** Get the list of supported login flows */ + QVector loginFlows() const; + /** Check whether the current homeserver supports password auth */ + bool supportsPasswordAuth() const; + /** Check whether the current homeserver supports SSO */ + bool supportsSso() const; /** Find a room by its id and a mask of applicable states */ Q_INVOKABLE Room* room(const QString& roomId, JoinStates states = JoinState::Invite|JoinState::Join) const; @@ -550,6 +581,7 @@ namespace QMatrixClient void resolveError(QString error); void homeserverChanged(QUrl baseUrl); + void loginFlowsChanged(); void capabilitiesLoaded(); void connected(); -- cgit v1.2.3 From 1b5d6216beddd17c820240dd4bdeaf9c47624cf6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 25 Mar 2020 22:24:04 +0100 Subject: Connection: loginWithToken(); connectWithToken() -> assumeIdentity() Part of #388 backport. --- lib/connection.cpp | 75 +++++++++++++++++++++++++++++++++++++++--------------- lib/connection.h | 23 ++++++++++++++++- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 99d5c04a..6da04118 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -111,8 +111,10 @@ class Connection::Private .value("cache_type").toString() != "json"; bool lazyLoading = false; - void connectWithToken(const QString& userId, const QString& accessToken, - const QString& deviceId); + template + void loginToServer(LoginArgTs&&... loginArgs); + void assumeIdentity(const QString& newUserId, const QString& accessToken, + const QString& deviceId); template EventT* unpackAccountData() const @@ -221,6 +223,19 @@ void Connection::resolveServer(const QString& mxidOrDomain) }); } +inline UserIdentifier makeUserIdentifier(const QString& id) +{ + return { QStringLiteral("m.id.user"), { { QStringLiteral("user"), id } } }; +} + +inline UserIdentifier make3rdPartyIdentifier(const QString& medium, + const QString& address) +{ + return { QStringLiteral("m.id.thirdparty"), + { { QStringLiteral("medium"), medium }, + { QStringLiteral("address"), address } } }; +} + void Connection::connectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId) @@ -230,23 +245,22 @@ void Connection::connectToServer(const QString& user, const QString& password, doConnectToServer(user, password, initialDeviceName, deviceId); }); } + void Connection::doConnectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId) { - auto loginJob = callApi(QStringLiteral("m.login.password"), - UserIdentifier { QStringLiteral("m.id.user"), - {{ QStringLiteral("user"), user }} }, - password, /*token*/ "", deviceId, initialDeviceName); - connect(loginJob, &BaseJob::success, this, - [this, loginJob] { - d->connectWithToken(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); - }); - connect(loginJob, &BaseJob::failure, this, - [this, loginJob] { - emit loginError(loginJob->errorString(), loginJob->rawDataSample()); - }); + d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(user), + password, /*token*/ "", deviceId, initialDeviceName); +} + +void Connection::loginWithToken(const QByteArray& loginToken, + const QString& initialDeviceName, + const QString& deviceId) +{ + d->loginToServer(LoginFlows::Token.type, + makeUserIdentifier(/*user is encoded in loginToken*/ {}), + /*password*/ "", loginToken, deviceId, initialDeviceName); } void Connection::syncLoopIteration() @@ -257,9 +271,16 @@ void Connection::syncLoopIteration() void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) +{ + assumeIdentity(userId, accessToken, deviceId); +} + +void Connection::assumeIdentity(const QString& userId, + const QString& accessToken, + const QString& deviceId) { checkAndConnect(userId, - [=] { d->connectWithToken(userId, accessToken, deviceId); }); + [=] { d->assumeIdentity(userId, accessToken, deviceId); }); } void Connection::reloadCapabilities() @@ -294,11 +315,25 @@ bool Connection::loadingCapabilities() const return d->capabilities.roomVersions.omitted(); } -void Connection::Private::connectWithToken(const QString& user, - const QString& accessToken, - const QString& deviceId) +template +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()); + }); + connect(loginJob, &BaseJob::failure, q, [this, loginJob] { + emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); + }); +} + +void Connection::Private::assumeIdentity(const QString& newUserId, + const QString& accessToken, + const QString& deviceId) { - userId = user; + userId = newUserId; q->user(); // Creates a User object for the local user data->setToken(accessToken.toLatin1()); data->setDeviceId(deviceId); diff --git a/lib/connection.h b/lib/connection.h index ad228bf0..782ffd2c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -403,6 +403,18 @@ namespace QMatrixClient std::forward(jobArgs)...); } + /** Get a request URL for a job with specified type and arguments + * + * This calls JobT::makeRequestUrl() prepending the connection's + * homeserver to the list of arguments. + */ + template + QUrl getUrlForApi(JobArgTs&&... jobArgs) const + { + return JobT::makeRequestUrl(homeserver(), + std::forward(jobArgs)...); + } + /** Generate a new transaction id. Transaction id's are unique within * a single Connection object */ @@ -438,7 +450,16 @@ namespace QMatrixClient void connectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId = {}); - void connectWithToken(const QString& userId, const QString& accessToken, + void loginWithToken(const QByteArray& loginToken, + const QString& initialDeviceName, + const QString& deviceId = {}); + void assumeIdentity(const QString& userId, const QString& accessToken, + const QString& 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); /// Explicitly request capabilities from the server void reloadCapabilities(); -- cgit v1.2.3 From 123d58fc3c29d8e9adbb5b654df53d5cbb0a32fa Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 26 Mar 2020 19:14:34 +0100 Subject: SsoSession and Connection::prepareForSso() Final part of #388 backport. --- CMakeLists.txt | 1 + lib/connection.cpp | 6 +++ lib/connection.h | 4 ++ lib/ssosession.cpp | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/ssosession.h | 44 ++++++++++++++++++ libqmatrixclient.pri | 2 + 6 files changed, 184 insertions(+) create mode 100644 lib/ssosession.cpp create mode 100644 lib/ssosession.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ae97a6c..ebde5186 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ set(libqmatrixclient_SRCS lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp + lib/ssosession.cpp lib/logging.cpp lib/room.cpp lib/user.cpp diff --git a/lib/connection.cpp b/lib/connection.cpp index 6da04118..647fc006 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -254,6 +254,12 @@ void Connection::doConnectToServer(const QString& user, const QString& password, password, /*token*/ "", deviceId, initialDeviceName); } +SsoSession* Connection::prepareForSso(const QString& initialDeviceName, + const QString& deviceId) +{ + return new SsoSession(this, initialDeviceName, deviceId); +} + void Connection::loginWithToken(const QByteArray& loginToken, const QString& initialDeviceName, const QString& deviceId) diff --git a/lib/connection.h b/lib/connection.h index 782ffd2c..33f9bfba 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -19,6 +19,7 @@ #pragma once #include "csapi/login.h" +#include "ssosession.h" #include "csapi/create_room.h" #include "joinstate.h" #include "events/accountdataevents.h" @@ -415,6 +416,9 @@ namespace QMatrixClient std::forward(jobArgs)...); } + Q_INVOKABLE SsoSession* prepareForSso( + const QString& initialDeviceName, const QString& deviceId = {}); + /** Generate a new transaction id. Transaction id's are unique within * a single Connection object */ diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp new file mode 100644 index 00000000..6ea4a3f5 --- /dev/null +++ b/lib/ssosession.cpp @@ -0,0 +1,127 @@ +#include "ssosession.h" + +#include "connection.h" +#include "csapi/sso_login_redirect.h" + +#include +#include +#include +#include + +using namespace QMatrixClient; + +struct SsoSession::Private { + Private(SsoSession* q, const QString& initialDeviceName = {}, + const QString& deviceId = {}, Connection* connection = nullptr) + : initialDeviceName(initialDeviceName) + , deviceId(deviceId) + , connection(connection) + { + auto* server = new QTcpServer(q); + server->listen(); + // The "/returnToApplication" part is just a hint for the end-user, + // the callback will work without it equally well. + callbackUrl = QStringLiteral("http://localhost:%1/returnToApplication") + .arg(server->serverPort()); + ssoUrl = connection->getUrlForApi(callbackUrl); + + QObject::connect(server, &QTcpServer::newConnection, q, [this, server] { + qCDebug(MAIN) << "SSO callback initiated"; + socket = server->nextPendingConnection(); + server->close(); + QObject::connect(socket, &QTcpSocket::readyRead, socket, [this] { + requestData.append(socket->readAll()); + if (!socket->atEnd() && !requestData.endsWith("\r\n\r\n")) { + qDebug(MAIN) << "Incomplete request, waiting for more data"; + return; + } + processCallback(); + }); + QObject::connect(socket, &QTcpSocket::disconnected, socket, + [this] { socket->deleteLater(); }); + }); + } + void processCallback(); + void sendHttpResponse(const QByteArray& code, const QByteArray& msg); + void onError(const QByteArray& code, const QString& errorMsg); + + QString initialDeviceName; + QString deviceId; + Connection* connection; + QString callbackUrl {}; + QUrl ssoUrl {}; + QTcpSocket* socket = nullptr; + QByteArray requestData {}; +}; + +SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName, + const QString& deviceId) + : QObject(connection) + , d(std::make_unique(this, initialDeviceName, deviceId, connection)) +{ + qCDebug(MAIN) << "SSO session constructed"; +} + +SsoSession::~SsoSession() +{ + qCDebug(MAIN) << "SSO session deconstructed"; +} + +QUrl SsoSession::ssoUrl() const { return d->ssoUrl; } + +QUrl SsoSession::callbackUrl() const { return d->callbackUrl; } + +void SsoSession::Private::processCallback() +{ + // https://matrix.org/docs/guides/sso-for-client-developers + // Inspired by Clementine's src/internet/core/localredirectserver.cpp + // (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")); + 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")); + } + 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"; + sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8()); + socket->disconnectFromHost(); + }); + connect(connection, &Connection::loginError, socket, [this] { + onError("401 Unauthorised", tr("Login failed")); + socket->disconnectFromHost(); + }); +} + +void SsoSession::Private::sendHttpResponse(const QByteArray& code, + const QByteArray& msg) +{ + socket->write("HTTP/1.0 "); + socket->write(code); + socket->write("\r\n" + "Content-type: text/html;charset=UTF-8\r\n" + "\r\n\r\n"); + socket->write(msg); + socket->write("\r\n"); +} + +void SsoSession::Private::onError(const QByteArray& code, + const QString& errorMsg) +{ + qCWarning(MAIN).nospace() << errorMsg; + sendHttpResponse(code, "

" + errorMsg.toUtf8() + "

"); + // [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); +} diff --git a/lib/ssosession.h b/lib/ssosession.h new file mode 100644 index 00000000..af20c075 --- /dev/null +++ b/lib/ssosession.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include + +class QTcpServer; +class QTcpSocket; + +namespace QMatrixClient { +class Connection; + +/*! Single sign-on (SSO) session encapsulation + * + * This class is responsible for setting up of a new SSO session, providing + * a URL to be opened (usually, in a web browser) and handling the callback + * response after completing the single sign-on, all the way to actually + * logging the user in. It does NOT open and render the SSO URL, it only does + * the necessary backstage work. + * + * Clients only need to open the URL; the rest is done for them. + * Client code can look something like: + * \code + * QDesktopServices::openUrl( + * connection->prepareForSso(initialDeviceName)->ssoUrl()); + * \endcode + */ +class SsoSession : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT) + Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT) +public: + SsoSession(Connection* connection, const QString& initialDeviceName, + const QString& deviceId = {}); + ~SsoSession() override; + QUrl ssoUrl() const; + QUrl callbackUrl() const; + +private: + class Private; + std::unique_ptr d; +}; +} // namespace QMatrixClient diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 79f1d50b..23459260 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -13,6 +13,7 @@ INCLUDEPATH += $$SRCPATH HEADERS += \ $$SRCPATH/connectiondata.h \ $$SRCPATH/connection.h \ + $$SRCPATH/ssosession.h \ $$SRCPATH/eventitem.h \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ @@ -61,6 +62,7 @@ HEADERS += \ SOURCES += \ $$SRCPATH/connectiondata.cpp \ $$SRCPATH/connection.cpp \ + $$SRCPATH/ssosession.cpp \ $$SRCPATH/eventitem.cpp \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ -- cgit v1.2.3 From 2216c5f844e5227976f4b0a8208c6c7e834934ae Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 26 Mar 2020 22:49:06 +0100 Subject: Connection: allRooms(), rooms(), roomsCount(); deprecate roomMap() Backport of #354. --- lib/connection.cpp | 49 +++++++++++++++++++++++++++++++++++++++---------- lib/connection.h | 27 +++++++++++++++++++++++++++ lib/user.cpp | 6 ++---- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 647fc006..8c87d775 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1046,6 +1046,33 @@ QHash< QPair, Room* > Connection::roomMap() const return roomMap; } +QVector Connection::allRooms() const +{ + QVector result; + result.resize(d->roomMap.size()); + std::copy(d->roomMap.cbegin(), d->roomMap.cend(), result.begin()); + return result; +} + +QVector Connection::rooms(JoinStates joinStates) const +{ + QVector result; + for (auto* r: qAsConst(d->roomMap)) + if (joinStates.testFlag(r->joinState())) + result.push_back(r); + return result; +} + +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(), + [joinStates](Room* r) { + return joinStates.testFlag(r->joinState()); + })); +} + bool Connection::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.cend(); @@ -1371,18 +1398,20 @@ void Connection::saveState() const { QStringLiteral("minor"), SyncData::cacheVersion().second } }}}; { - QJsonObject rooms; - QJsonObject inviteRooms; - const auto& rs = roomMap(); // Pass on rooms in Leave state - for (const auto* i : rs) - (i->joinState() == JoinState::Invite ? inviteRooms : rooms) - .insert(i->id(), QJsonValue::Null); + QJsonObject roomsJson; + QJsonObject inviteRoomsJson; + for (const auto* r: qAsConst(d->roomMap)) { + if (r->joinState() == JoinState::Leave) + continue; + (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson) + .insert(r->id(), QJsonValue::Null); + } QJsonObject roomObj; - if (!rooms.isEmpty()) - roomObj.insert(QStringLiteral("join"), rooms); - if (!inviteRooms.isEmpty()) - roomObj.insert(QStringLiteral("invite"), inviteRooms); + if (!roomsJson.isEmpty()) + roomObj.insert(QStringLiteral("join"), roomsJson); + if (!inviteRoomsJson.isEmpty()) + roomObj.insert(QStringLiteral("invite"), inviteRoomsJson); rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); rootObj.insert(QStringLiteral("rooms"), roomObj); diff --git a/lib/connection.h b/lib/connection.h index 33f9bfba..b0dfeb5e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -154,11 +154,38 @@ namespace QMatrixClient virtual ~Connection(); /** Get all Invited and Joined rooms + * + * \deprecated + * Use allRooms(), roomsWithTag(), or rooms(JoinStates) instead * \return a hashmap from a composite key - room name and whether * it's an Invite rather than Join - to room pointers */ QHash, Room*> roomMap() const; + /** Get all rooms known within this Connection + * + * This includes Invite, Join and Leave rooms, in no particular order. + * \note Leave rooms will only show up in the list if they have been left + * in the same running session. The library doesn't cache left rooms + * between runs and it doesn't retrieve the full list of left rooms + * from the server. + * \sa rooms, room, roomsWithTag + */ + Q_INVOKABLE QVector allRooms() const; + + /** Get rooms that have either of the given join state(s) + * + * This method returns, in no particular order, rooms which join state + * matches the mask passed in \p joinStates. + * \note Similar to allRooms(), this won't retrieve the full list of + * Leave rooms from the server. + * \sa allRooms, room, roomsWithTag + */ + Q_INVOKABLE QVector rooms(JoinStates joinStates) const; + + /** Get the total number of rooms in the given join state(s) */ + Q_INVOKABLE int roomsCount(JoinStates joinStates) const; + /** Check whether the account has data of the given type * Direct chats map is not supported by this method _yet_. */ diff --git a/lib/user.cpp b/lib/user.cpp index 535551cf..c51354a0 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -118,8 +118,7 @@ void User::Private::setNameForRoom(const Room* r, QString newName, et.start(); } - const auto& roomMap = connection->roomMap(); - for (auto* r1: roomMap) + for (auto* r1: connection->allRooms()) if (nameForRoom(r1) == mostUsedName) otherNames.insert(mostUsedName, r1); @@ -186,8 +185,7 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, nextMostUsedIt = otherAvatars.end() - 1; } std::swap(mostUsedAvatar, *nextMostUsedIt); - const auto& roomMap = connection->roomMap(); - for (const auto* r1: roomMap) + for (const auto* r1: connection->allRooms()) if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) avatarsToRooms.insert(nextMostUsedIt->url(), r1); -- cgit v1.2.3 From 0591b3d58f7cff90fd6dcfb54c01f04b6a4549a4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Mar 2020 17:30:48 +0100 Subject: CMakeLists.txt: update version to 0.5.3 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ebde5186..34afb2dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1) set(API_VERSION "0.5.1") # Normally it should just include major.minor -project(qmatrixclient VERSION "0.5.2" LANGUAGES CXX) +project(qmatrixclient VERSION "0.5.3" LANGUAGES CXX) option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) -- cgit v1.2.3 From f5876b7c9fbaf07696605929e37f9921ce6e745e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Mar 2020 18:07:13 +0100 Subject: Fix compatibility with MSVC 2015 --- lib/events/reactionevent.h | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index a422abeb..7a4c9b5a 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -22,8 +22,11 @@ namespace QMatrixClient { -struct EventRelation -{ +struct EventRelation { + // To please MSVC 2015 that doesn't handle initialiser lists like proper + EventRelation(QString type = {}, QString eventId = {}, QString key = {}) + : type(std::move(type)), eventId(std::move(eventId)), key(std::move(key)) + {} using reltypeid_t = const char*; static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } static constexpr reltypeid_t Annotation() { return "m.annotation"; } @@ -35,15 +38,15 @@ struct EventRelation static EventRelation replyTo(QString eventId) { - return EventRelation { Reply(), std::move(eventId) }; + return EventRelation(Reply(), std::move(eventId)); } static EventRelation annotate(QString eventId, QString key) { - return EventRelation { Annotation(), std::move(eventId), std::move(key) }; + return EventRelation(Annotation(), std::move(eventId), std::move(key)); } static EventRelation replace(QString eventId) { - return EventRelation { Replacement(), std::move(eventId) }; + return EventRelation(Replacement(), std::move(eventId)); } }; template <> @@ -70,8 +73,8 @@ public: return content(QStringLiteral("m.relates_to")); } -private: - EventRelation _relation; +//private: +// EventRelation _relation; }; REGISTER_EVENT_TYPE(ReactionEvent) -- cgit v1.2.3 From bd14854f1d4790aeea5e39ee6ceabbd690331761 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 30 Mar 2020 19:12:06 +0200 Subject: BaseJob: shutdown timers on abandoning and destruction A part of the fix for #398. --- lib/jobs/basejob.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a1f49b63..30d60f9e 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -104,6 +104,7 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, BaseJob::~BaseJob() { stop(); + d->retryTimer.stop(); // See #398 qCDebug(d->logCat) << this << "destroyed"; } @@ -641,6 +642,8 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { beforeAbandon(d->reply ? d->reply.data() : nullptr); + d->timer.stop(); + d->retryTimer.stop(); // In case abandon() was called between retries setStatus(Abandoned); if (d->reply) d->reply->disconnect(this); -- cgit v1.2.3 From 41a74ae1175b4b8723ef829270f7149c302a13c1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 30 Mar 2020 19:12:40 +0200 Subject: BaseJob: validate the connection health before running the request --- lib/jobs/basejob.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 30d60f9e..0e6a8403 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -211,6 +211,7 @@ void BaseJob::Private::sendRequest(bool inBackground) // some sources claim that there are issues with QT 5.8 req.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); #endif + Q_ASSERT(req.url().isValid()); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); switch( verb ) @@ -241,16 +242,23 @@ void BaseJob::beforeAbandon(QNetworkReply*) void BaseJob::start(const ConnectionData* connData, bool inBackground) { - d->connection = connData; - d->retryTimer.setSingleShot(true); - connect (&d->retryTimer, &QTimer::timeout, - this, [this,inBackground] { sendRequest(inBackground); }); - - beforeStart(connData); - if (status().good()) - sendRequest(inBackground); - if (status().good()) - afterStart(connData, d->reply.data()); + if (connData && connData->baseUrl().isValid()) { + d->connection = connData; + d->retryTimer.setSingleShot(true); + connect(&d->retryTimer, &QTimer::timeout, this, + [this, inBackground] { sendRequest(inBackground); }); + + beforeStart(connData); + if (status().good()) + sendRequest(inBackground); + if (status().good()) + afterStart(connData, d->reply.data()); + } else { + qCCritical(d->logCat) + << "Developers, ensure the Connection is valid before using it"; + Q_ASSERT(false); + setStatus(IncorrectRequestError, tr("Invalid server connection")); + } if (!status().good()) QTimer::singleShot(0, this, &BaseJob::finishJob); } -- cgit v1.2.3 From cfbc71bc70707ff1ed691cc04ecbe246024f6734 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 30 Mar 2020 19:16:11 +0200 Subject: Connection::resolveServer(): refactor Also: use 4-arg connect() to make sure lambdas are disconnected if the connection is gone. --- lib/connection.cpp | 75 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 8c87d775..cc140ae4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -175,52 +175,55 @@ void Connection::resolveServer(const QString& mxidOrDomain) maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" if (!match.hasMatch() || !maybeBaseUrl.isValid()) { - emit resolveError( - tr("%1 is not a valid homeserver address") - .arg(maybeBaseUrl.toString())); + emit resolveError(tr("%1 is not a valid homeserver address") + .arg(maybeBaseUrl.toString())); return; } - setHomeserver(maybeBaseUrl); - auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; + d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file auto getWellKnownJob = callApi(); - connect(getWellKnownJob, &BaseJob::finished, [this, getWellKnownJob, maybeBaseUrl] { - if (getWellKnownJob->status() == BaseJob::NotFoundError) { - qCDebug(MAIN) << "No .well-known file, IGNORE"; - } else if (getWellKnownJob->status() != BaseJob::Success) { - qCDebug(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; - emit resolveError(tr("Fetching .well-known file failed")); - return; - } else if (getWellKnownJob->data().homeserver.baseUrl.isEmpty()) { - qCDebug(MAIN) << "base_url not provided, FAIL_PROMPT"; - emit resolveError(tr("base_url not provided")); - return; - } else if (!QUrl(getWellKnownJob->data().homeserver.baseUrl).isValid()) { - qCDebug(MAIN) << "base_url invalid, FAIL_ERROR"; - emit resolveError(tr("base_url invalid")); - return; - } else { - QUrl baseUrl(getWellKnownJob->data().homeserver.baseUrl); - - qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.authority(); - setHomeserver(baseUrl); - } - - auto getVersionsJob = callApi(); - - connect(getVersionsJob, &BaseJob::finished, [this, getVersionsJob] { - if (getVersionsJob->status() == BaseJob::Success) { - qCDebug(MAIN) << "homeserver url is valid"; - emit resolved(); + connect(getWellKnownJob, &BaseJob::finished, this, + [this, getWellKnownJob, maybeBaseUrl] { + if (getWellKnownJob->status() != BaseJob::NotFoundError) { + if (getWellKnownJob->status() != BaseJob::Success) { + qCWarning(MAIN) + << "Fetching .well-known file failed, FAIL_PROMPT"; + emit resolveError(tr("Failed resolving the homeserver")); + return; + } + QUrl baseUrl { getWellKnownJob->data().homeserver.baseUrl }; + if (baseUrl.isEmpty()) { + qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT"; + emit resolveError( + tr("The homeserver base URL is not provided")); + return; + } + if (!baseUrl.isValid()) { + qCWarning(MAIN) << "base_url invalid, FAIL_ERROR"; + emit resolveError(tr("The homeserver base URL is invalid")); + return; + } + qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() + << "is" << baseUrl.authority(); + setHomeserver(baseUrl); } else { - qCDebug(MAIN) << "homeserver url invalid"; - emit resolveError(tr("homeserver url invalid")); + qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl + << "for base URL"; + setHomeserver(maybeBaseUrl); } + + auto getVersionsJob = callApi(); + connect(getVersionsJob, &BaseJob::success, this, + &Connection::resolved); + connect(getVersionsJob, &BaseJob::failure, this, [this] { + qCWarning(MAIN) << "Homeserver base URL invalid"; + emit resolveError(tr("The homeserver base URL " + "doesn't seem to work")); + }); }); - }); } inline UserIdentifier makeUserIdentifier(const QString& id) -- cgit v1.2.3 From 08cc50a3e6699e4d0cff60108260adbd8cbdf419 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 30 Mar 2020 19:41:59 +0200 Subject: Connection::resolveServer(): reparent jobs to ensure proper clean-away This is a spot fix for #398; Quotient 0.6 will reparent all jobs to make sure they are removed after the connection is gone (this also partially helps with #397). --- lib/connection.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index cc140ae4..0c98c383 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -185,6 +185,10 @@ void Connection::resolveServer(const QString& mxidOrDomain) d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file auto getWellKnownJob = callApi(); + // This is a workaround for 0.5.x; due to the way Quaternion's login dialog + // operates, Connection can disappear any moment during server resolution. + // Quotient 0.6 will reparent all jobs to enforce lifetimes. See also #398. + getWellKnownJob->setParent(this); connect(getWellKnownJob, &BaseJob::finished, this, [this, getWellKnownJob, maybeBaseUrl] { if (getWellKnownJob->status() != BaseJob::NotFoundError) { @@ -216,6 +220,7 @@ void Connection::resolveServer(const QString& mxidOrDomain) } auto getVersionsJob = callApi(); + getVersionsJob->setParent(this); // Same workaround as above connect(getVersionsJob, &BaseJob::success, this, &Connection::resolved); connect(getVersionsJob, &BaseJob::failure, this, [this] { -- cgit v1.2.3 From f0c5016656c4b2ddd26be5522b735a7959469ed1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 31 Mar 2020 07:46:55 +0200 Subject: Version 0.5.3.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34afb2dc..fc54cd81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1) set(API_VERSION "0.5.1") # Normally it should just include major.minor -project(qmatrixclient VERSION "0.5.3" LANGUAGES CXX) +project(qmatrixclient VERSION "0.5.3.1" LANGUAGES CXX) option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) -- cgit v1.2.3 From c4bbd187b8dac480230848906c31c4a0bc24663e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 1 Apr 2020 18:49:48 +0200 Subject: CMakeLists.txt: bump API_VERSION; prepare for 0.5.3.2 Because new API has been introduced in 0.5.3. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fc54cd81..e503bc5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1) -set(API_VERSION "0.5.1") # Normally it should just include major.minor -project(qmatrixclient VERSION "0.5.3.1" LANGUAGES CXX) +set(API_VERSION "0.5.3") # Normally it should just include major.minor +project(qmatrixclient VERSION "0.5.3.2" LANGUAGES CXX) option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) -- cgit v1.2.3 -- cgit v1.2.3